Update part4 and part5
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
"evancz/url-parser": "2.0.1 <= v < 3.0.0",
|
"evancz/url-parser": "2.0.1 <= v < 3.0.0",
|
||||||
"lukewestby/elm-http-builder": "5.1.0 <= v < 6.0.0",
|
"lukewestby/elm-http-builder": "5.1.0 <= v < 6.0.0",
|
||||||
"mgold/elm-date-format": "1.3.0 <= v < 2.0.0",
|
"mgold/elm-date-format": "1.3.0 <= v < 2.0.0",
|
||||||
"rtfeldman/elm-validate": "2.0.0 <= v < 3.0.0",
|
"rtfeldman/elm-validate": "3.0.0 <= v < 4.0.0",
|
||||||
"rtfeldman/selectlist": "1.0.0 <= v < 2.0.0"
|
"rtfeldman/selectlist": "1.0.0 <= v < 2.0.0"
|
||||||
},
|
},
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
"elm-version": "0.18.0 <= v < 0.19.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"rtfeldman/elm-validate": "2.0.0",
|
"rtfeldman/elm-validate": "3.0.0",
|
||||||
"rtfeldman/selectlist": "1.0.0",
|
"rtfeldman/selectlist": "1.0.0",
|
||||||
"elm-lang/navigation": "2.1.0",
|
"elm-lang/navigation": "2.1.0",
|
||||||
"elm-lang/virtual-dom": "2.0.4",
|
"elm-lang/virtual-dom": "2.0.4",
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"summary": "Convenience functions for validating data.",
|
|
||||||
"repository": "https://github.com/rtfeldman/elm-validate.git",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"source-directories": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [
|
|
||||||
"Validate"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
module Validate
|
|
||||||
exposing
|
|
||||||
( Validator
|
|
||||||
, all
|
|
||||||
, any
|
|
||||||
, firstError
|
|
||||||
, ifBlank
|
|
||||||
, ifEmptyDict
|
|
||||||
, ifEmptyList
|
|
||||||
, ifEmptySet
|
|
||||||
, ifFalse
|
|
||||||
, ifInvalidEmail
|
|
||||||
, ifNotInt
|
|
||||||
, ifNothing
|
|
||||||
, ifTrue
|
|
||||||
, isBlank
|
|
||||||
, isInt
|
|
||||||
, isValidEmail
|
|
||||||
, validate
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| Convenience functions for validating data.
|
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifNotInt, validate)
|
|
||||||
|
|
||||||
type Field = Name | Email | Age
|
|
||||||
|
|
||||||
type alias Model = { name : String, email : String, age : String }
|
|
||||||
|
|
||||||
modelValidator : Validator String Model
|
|
||||||
modelValidator =
|
|
||||||
Validate.all
|
|
||||||
[ ifBlank .name "Please enter a name."
|
|
||||||
, Validate.firstError
|
|
||||||
[ ifBlank .email "Please enter an email address."
|
|
||||||
, ifInvalidEmail .email "This is not a valid email address."
|
|
||||||
]
|
|
||||||
, ifNotInt .age "Age must be a whole number."
|
|
||||||
]
|
|
||||||
|
|
||||||
validate modelValidator { name = "Sam", email = "blah", age = "abc" }
|
|
||||||
--> [ "This is not a valid email address.", "Age must be a whole number." ]
|
|
||||||
|
|
||||||
|
|
||||||
# Validating a subject
|
|
||||||
|
|
||||||
@docs Validator, validate
|
|
||||||
|
|
||||||
|
|
||||||
# Creating validators
|
|
||||||
|
|
||||||
@docs ifBlank, ifNotInt, ifEmptyList, ifEmptyDict, ifEmptySet, ifNothing, ifInvalidEmail, ifTrue, ifFalse
|
|
||||||
|
|
||||||
|
|
||||||
# Combining validators
|
|
||||||
|
|
||||||
@docs all, any, firstError
|
|
||||||
|
|
||||||
|
|
||||||
# Checking values directly
|
|
||||||
|
|
||||||
@docs isBlank, isInt, isValidEmail
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Dict exposing (Dict)
|
|
||||||
import Regex exposing (Regex)
|
|
||||||
import Set exposing (Set)
|
|
||||||
import String
|
|
||||||
|
|
||||||
|
|
||||||
-- VALIDATING A SUBJECT --
|
|
||||||
|
|
||||||
|
|
||||||
{-| A `Validator` contains a function which takes a subject and returns a list
|
|
||||||
of errors describing anything invalid about that subject.
|
|
||||||
|
|
||||||
Pass it to [`validate`](#validate) to get the list of errors.
|
|
||||||
An empty error list means the subject was valid.
|
|
||||||
|
|
||||||
-}
|
|
||||||
type Validator error subject
|
|
||||||
= Validator (subject -> List error)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if the given predicate returns `True` for the given
|
|
||||||
subject.
|
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifNotInt, validate)
|
|
||||||
|
|
||||||
type Field = Name | Email | Age
|
|
||||||
|
|
||||||
type alias Model = { name : String, email : String, age : String }
|
|
||||||
|
|
||||||
modelValidator : Validator ( Field, String ) Model
|
|
||||||
modelValidator =
|
|
||||||
Validate.all
|
|
||||||
[ ifBlank .name ( Name, "Please enter a name." )
|
|
||||||
, ifBlank .email ( Email, "Please enter an email address." )
|
|
||||||
, ifNotInt .age ( Age, "Age must be a whole number." )
|
|
||||||
]
|
|
||||||
|
|
||||||
validate modelValidator { name = "Sam", email = "", age = "abc" }
|
|
||||||
--> [ ( Email, "Please enter an email address." ), ( Age, "Age must be a whole number." ) ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
validate : Validator error subject -> subject -> List error
|
|
||||||
validate (Validator getErrors) subject =
|
|
||||||
getErrors subject
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- CONSTRUCTING VALIDATORS --
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if the given `String` is empty, or if it contains only
|
|
||||||
whitespace characters.
|
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifNotInt)
|
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
|
||||||
modelValidator =
|
|
||||||
Validate.all
|
|
||||||
[ ifBlank .name "Please enter a name."
|
|
||||||
, ifBlank .email "Please enter an email address."
|
|
||||||
]
|
|
||||||
|
|
||||||
-}
|
|
||||||
ifBlank : (subject -> String) -> error -> Validator error subject
|
|
||||||
ifBlank subjectToString error =
|
|
||||||
ifTrue (\subject -> isBlank (subjectToString subject)) error
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if the given `String` cannot be parsed as an `Int`.
|
|
||||||
-}
|
|
||||||
ifNotInt : (subject -> String) -> error -> Validator error subject
|
|
||||||
ifNotInt subjectToString error =
|
|
||||||
ifFalse (\subject -> isInt (subjectToString subject)) error
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if a `List` is empty.
|
|
||||||
-}
|
|
||||||
ifEmptyList : (subject -> List a) -> error -> Validator error subject
|
|
||||||
ifEmptyList subjectToList error =
|
|
||||||
ifTrue (\subject -> List.isEmpty (subjectToList subject)) error
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if a `Dict` is empty.
|
|
||||||
-}
|
|
||||||
ifEmptyDict : (subject -> Dict comparable v) -> error -> Validator error subject
|
|
||||||
ifEmptyDict subjectToDict error =
|
|
||||||
ifTrue (\subject -> Dict.isEmpty (subjectToDict subject)) error
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if a `Set` is empty.
|
|
||||||
-}
|
|
||||||
ifEmptySet : (subject -> Set comparable) -> error -> Validator error subject
|
|
||||||
ifEmptySet subjectToSet error =
|
|
||||||
ifTrue (\subject -> Set.isEmpty (subjectToSet subject)) error
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if a `Maybe` is `Nothing`.
|
|
||||||
-}
|
|
||||||
ifNothing : (subject -> Maybe a) -> error -> Validator error subject
|
|
||||||
ifNothing subjectToMaybe error =
|
|
||||||
ifTrue (\subject -> subjectToMaybe subject == Nothing) error
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if an email address is malformed.
|
|
||||||
-}
|
|
||||||
ifInvalidEmail : (subject -> String) -> error -> Validator error subject
|
|
||||||
ifInvalidEmail subjectToEmail error =
|
|
||||||
ifFalse (\subject -> isValidEmail (subjectToEmail subject)) error
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if a predicate returns `True` for the given
|
|
||||||
subject.
|
|
||||||
|
|
||||||
import Validate exposing (ifTrue)
|
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
|
||||||
modelValidator =
|
|
||||||
ifTrue (\model -> countSelected model < 2)
|
|
||||||
"Please select at least two."
|
|
||||||
|
|
||||||
-}
|
|
||||||
ifTrue : (subject -> Bool) -> error -> Validator error subject
|
|
||||||
ifTrue test error =
|
|
||||||
let
|
|
||||||
getErrors subject =
|
|
||||||
if test subject then
|
|
||||||
[ error ]
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
in
|
|
||||||
Validator getErrors
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if a predicate returns `False` for the given
|
|
||||||
subject.
|
|
||||||
|
|
||||||
import Validate exposing (ifFalse)
|
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
|
||||||
modelValidator =
|
|
||||||
ifFalse (\model -> countSelected model >= 2)
|
|
||||||
"Please select at least two."
|
|
||||||
|
|
||||||
-}
|
|
||||||
ifFalse : (subject -> Bool) -> error -> Validator error subject
|
|
||||||
ifFalse test error =
|
|
||||||
let
|
|
||||||
getErrors subject =
|
|
||||||
if test subject then
|
|
||||||
[]
|
|
||||||
else
|
|
||||||
[ error ]
|
|
||||||
in
|
|
||||||
Validator getErrors
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- COMBINING VALIDATORS --
|
|
||||||
|
|
||||||
|
|
||||||
{-| Run each of the given validators, in order, and return their concatenated
|
|
||||||
error lists.
|
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifNotInt)
|
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
|
||||||
modelValidator =
|
|
||||||
Validate.all
|
|
||||||
[ ifBlank .name "Please enter a name."
|
|
||||||
, ifBlank .email "Please enter an email address."
|
|
||||||
, ifNotInt .age "Age must be a whole number."
|
|
||||||
]
|
|
||||||
|
|
||||||
-}
|
|
||||||
all : List (Validator error subject) -> Validator error subject
|
|
||||||
all validators =
|
|
||||||
let
|
|
||||||
newGetErrors subject =
|
|
||||||
let
|
|
||||||
accumulateErrors (Validator getErrors) totalErrors =
|
|
||||||
totalErrors ++ getErrors subject
|
|
||||||
in
|
|
||||||
List.foldl accumulateErrors [] validators
|
|
||||||
in
|
|
||||||
Validator newGetErrors
|
|
||||||
|
|
||||||
|
|
||||||
{-| Run each of the given validators, in order, stopping after the first error
|
|
||||||
and returning it. If no errors are encountered, return `Nothing`.
|
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifInvalidEmail, ifNotInt)
|
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
|
||||||
{ email : String, age : String }
|
|
||||||
|
|
||||||
|
|
||||||
modelValidator : Validator String Model
|
|
||||||
modelValidator =
|
|
||||||
Validate.all
|
|
||||||
[ Validate.firstError
|
|
||||||
[ ifBlank .email "Please enter an email address."
|
|
||||||
, ifInvalidEmail .email "This is not a valid email address."
|
|
||||||
]
|
|
||||||
, ifNotInt .age "Age must be a whole number."
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
validate modelValidator { email = " ", age = "5" }
|
|
||||||
--> [ "Please enter an email address." ]
|
|
||||||
|
|
||||||
validate modelValidator { email = "blah", age = "5" }
|
|
||||||
--> [ "This is not a valid email address." ]
|
|
||||||
|
|
||||||
validate modelValidator { email = "foo@bar.com", age = "5" }
|
|
||||||
--> []
|
|
||||||
|
|
||||||
-}
|
|
||||||
firstError : List (Validator error subject) -> Validator error subject
|
|
||||||
firstError validators =
|
|
||||||
let
|
|
||||||
getErrors subject =
|
|
||||||
firstErrorHelp validators subject
|
|
||||||
in
|
|
||||||
Validator getErrors
|
|
||||||
|
|
||||||
|
|
||||||
firstErrorHelp : List (Validator error subject) -> subject -> List error
|
|
||||||
firstErrorHelp validators subject =
|
|
||||||
case validators of
|
|
||||||
[] ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
(Validator getErrors) :: rest ->
|
|
||||||
case getErrors subject of
|
|
||||||
[] ->
|
|
||||||
firstErrorHelp rest subject
|
|
||||||
|
|
||||||
errors ->
|
|
||||||
errors
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return `True` if none of the given validators returns any errors for the given
|
|
||||||
subject, and `False` if any validator returns one or more errors.
|
|
||||||
-}
|
|
||||||
any : List (Validator error subject) -> subject -> Bool
|
|
||||||
any validators subject =
|
|
||||||
case validators of
|
|
||||||
[] ->
|
|
||||||
True
|
|
||||||
|
|
||||||
(Validator getErrors) :: others ->
|
|
||||||
case getErrors subject of
|
|
||||||
[] ->
|
|
||||||
any others subject
|
|
||||||
|
|
||||||
error :: _ ->
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- CHECKING VALUES DIRECTLY --
|
|
||||||
|
|
||||||
|
|
||||||
{-| Returns `True` if the given string is nothing but whitespace.
|
|
||||||
|
|
||||||
[`ifBlank`](#ifBlank) uses this under the hood.
|
|
||||||
|
|
||||||
-}
|
|
||||||
isBlank : String -> Bool
|
|
||||||
isBlank str =
|
|
||||||
Regex.contains lacksNonWhitespaceChars str
|
|
||||||
|
|
||||||
|
|
||||||
{-| Returns `True` if the email is malformed.
|
|
||||||
|
|
||||||
[`ifInvalidEmail`](#ifInvalidEmail) uses this under the hood.
|
|
||||||
|
|
||||||
-}
|
|
||||||
isValidEmail : String -> Bool
|
|
||||||
isValidEmail email =
|
|
||||||
Regex.contains validEmail email
|
|
||||||
|
|
||||||
|
|
||||||
{-| Returns `True` if `String.toInt` on the given string returns an `Ok`.
|
|
||||||
|
|
||||||
[`ifNotInt`](#ifNotInt) uses this under the hood.
|
|
||||||
|
|
||||||
-}
|
|
||||||
isInt : String -> Bool
|
|
||||||
isInt str =
|
|
||||||
case String.toInt str of
|
|
||||||
Ok _ ->
|
|
||||||
True
|
|
||||||
|
|
||||||
Err _ ->
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- INTERNAL HELPERS --
|
|
||||||
|
|
||||||
|
|
||||||
lacksNonWhitespaceChars : Regex
|
|
||||||
lacksNonWhitespaceChars =
|
|
||||||
Regex.regex "^\\s*$"
|
|
||||||
|
|
||||||
|
|
||||||
validEmail : Regex
|
|
||||||
validEmail =
|
|
||||||
Regex.regex "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
|
|
||||||
|> Regex.caseInsensitive
|
|
||||||
@@ -16,7 +16,7 @@ type alias Model =
|
|||||||
{ name : String, email : String, age : String, selections : List String }
|
{ name : String, email : String, age : String, selections : List String }
|
||||||
|
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
modelValidator : Validator String Model
|
||||||
modelValidator =
|
modelValidator =
|
||||||
Validate.all
|
Validate.all
|
||||||
[ ifBlank .name "Please enter a name."
|
[ ifBlank .name "Please enter a name."
|
||||||
@@ -28,8 +28,7 @@ modelValidator =
|
|||||||
|
|
||||||
validate modelValidator
|
validate modelValidator
|
||||||
{ name = "Sam", email = "", age = "abc", selections = [ "cats" ] }
|
{ name = "Sam", email = "", age = "abc", selections = [ "cats" ] }
|
||||||
== [ "Please enter an email address.", "Age must be a whole number." ]
|
--> [ "Please enter an email address.", "Age must be a whole number." ]
|
||||||
--> True
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can represent your errors however you like. One nice approach is to use
|
You can represent your errors however you like. One nice approach is to use
|
||||||
@@ -56,8 +55,7 @@ type alias Model =
|
|||||||
|
|
||||||
validate modelValidator
|
validate modelValidator
|
||||||
{ name = "Sam", email = "", age = "abc", selections = [ "cats" ] }
|
{ name = "Sam", email = "", age = "abc", selections = [ "cats" ] }
|
||||||
== [ ( Email, "Please enter an email address." )
|
--> [ ( Email, "Please enter an email address." )
|
||||||
, ( Age, "Age must be a whole number." )
|
--> , ( Age, "Age must be a whole number." )
|
||||||
]
|
--> ]
|
||||||
--> True
|
|
||||||
```
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"summary": "Convenience functions for validating data.",
|
"summary": "Convenience functions for validating data.",
|
||||||
"repository": "https://github.com/rtfeldman/elm-validate.git",
|
"repository": "https://github.com/rtfeldman/elm-validate.git",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
@@ -4,6 +4,7 @@ module Validate
|
|||||||
, all
|
, all
|
||||||
, any
|
, any
|
||||||
, firstError
|
, firstError
|
||||||
|
, fromErrors
|
||||||
, ifBlank
|
, ifBlank
|
||||||
, ifEmptyDict
|
, ifEmptyDict
|
||||||
, ifEmptyList
|
, ifEmptyList
|
||||||
@@ -21,7 +22,7 @@ module Validate
|
|||||||
|
|
||||||
{-| Convenience functions for validating data.
|
{-| Convenience functions for validating data.
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifNotInt, validate)
|
import Validate exposing (Validator, ifBlank, ifNotInt, validate)
|
||||||
|
|
||||||
type Field = Name | Email | Age
|
type Field = Name | Email | Age
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ module Validate
|
|||||||
|
|
||||||
# Creating validators
|
# Creating validators
|
||||||
|
|
||||||
@docs ifBlank, ifNotInt, ifEmptyList, ifEmptyDict, ifEmptySet, ifNothing, ifInvalidEmail, ifTrue, ifFalse
|
@docs ifBlank, ifNotInt, ifEmptyList, ifEmptyDict, ifEmptySet, ifNothing, ifInvalidEmail, ifTrue, ifFalse, fromErrors
|
||||||
|
|
||||||
|
|
||||||
# Combining validators
|
# Combining validators
|
||||||
@@ -86,7 +87,7 @@ type Validator error subject
|
|||||||
{-| Return an error if the given predicate returns `True` for the given
|
{-| Return an error if the given predicate returns `True` for the given
|
||||||
subject.
|
subject.
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifNotInt, validate)
|
import Validate exposing (Validator, ifBlank, ifNotInt, validate)
|
||||||
|
|
||||||
type Field = Name | Email | Age
|
type Field = Name | Email | Age
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ validate (Validator getErrors) subject =
|
|||||||
{-| Return an error if the given `String` is empty, or if it contains only
|
{-| Return an error if the given `String` is empty, or if it contains only
|
||||||
whitespace characters.
|
whitespace characters.
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifNotInt)
|
import Validate exposing (Validator, ifBlank)
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
modelValidator : Validator Model String
|
||||||
modelValidator =
|
modelValidator =
|
||||||
@@ -132,10 +133,31 @@ ifBlank subjectToString error =
|
|||||||
|
|
||||||
|
|
||||||
{-| Return an error if the given `String` cannot be parsed as an `Int`.
|
{-| Return an error if the given `String` cannot be parsed as an `Int`.
|
||||||
|
|
||||||
|
import Validate exposing (Validator, ifNotInt)
|
||||||
|
|
||||||
|
modelValidator : Validator Model String
|
||||||
|
modelValidator =
|
||||||
|
Validate.all
|
||||||
|
[ ifNotInt .followers (\_ -> "Please enter a whole number for followers.")
|
||||||
|
, ifNotInt .stars (\stars -> "Stars was \"" ++ stars ++ "\", but it needs to be a whole number.")"
|
||||||
|
]
|
||||||
|
|
||||||
-}
|
-}
|
||||||
ifNotInt : (subject -> String) -> error -> Validator error subject
|
ifNotInt : (subject -> String) -> (String -> error) -> Validator error subject
|
||||||
ifNotInt subjectToString error =
|
ifNotInt subjectToString errorFromString =
|
||||||
ifFalse (\subject -> isInt (subjectToString subject)) error
|
let
|
||||||
|
getErrors subject =
|
||||||
|
let
|
||||||
|
str =
|
||||||
|
subjectToString subject
|
||||||
|
in
|
||||||
|
if isInt str then
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
[ errorFromString str ]
|
||||||
|
in
|
||||||
|
Validator getErrors
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if a `List` is empty.
|
{-| Return an error if a `List` is empty.
|
||||||
@@ -167,16 +189,65 @@ ifNothing subjectToMaybe error =
|
|||||||
|
|
||||||
|
|
||||||
{-| Return an error if an email address is malformed.
|
{-| Return an error if an email address is malformed.
|
||||||
|
|
||||||
|
import Validate exposing (Validator, ifBlank, ifNotInt)
|
||||||
|
|
||||||
|
modelValidator : Validator Model String
|
||||||
|
modelValidator =
|
||||||
|
Validate.all
|
||||||
|
[ ifInvalidEmail .primaryEmail (\_ -> "Please enter a valid primary email address.")
|
||||||
|
, ifInvalidEmail .superSecretEmail (\email -> "Unfortunately, \"" ++ email ++ "\" is not a valid Super Secret Email Address.")
|
||||||
|
]
|
||||||
|
|
||||||
-}
|
-}
|
||||||
ifInvalidEmail : (subject -> String) -> error -> Validator error subject
|
ifInvalidEmail : (subject -> String) -> (String -> error) -> Validator error subject
|
||||||
ifInvalidEmail subjectToEmail error =
|
ifInvalidEmail subjectToEmail errorFromEmail =
|
||||||
ifFalse (\subject -> isValidEmail (subjectToEmail subject)) error
|
let
|
||||||
|
getErrors subject =
|
||||||
|
let
|
||||||
|
email =
|
||||||
|
subjectToEmail subject
|
||||||
|
in
|
||||||
|
if isValidEmail email then
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
[ errorFromEmail email ]
|
||||||
|
in
|
||||||
|
Validator getErrors
|
||||||
|
|
||||||
|
|
||||||
|
{-| Create a custom validator, by providing a function that returns a list of
|
||||||
|
errors given a subject.
|
||||||
|
|
||||||
|
import Validate exposing (Validator, fromErrors)
|
||||||
|
|
||||||
|
modelValidator : Validator Model String
|
||||||
|
modelValidator =
|
||||||
|
fromErrors modelToErrors
|
||||||
|
|
||||||
|
modelToErrors : Model -> List String
|
||||||
|
modelToErrors model =
|
||||||
|
let
|
||||||
|
usernameLength =
|
||||||
|
String.length model.username
|
||||||
|
in
|
||||||
|
if usernameLength < minUsernameChars then
|
||||||
|
[ "Username not long enough" ]
|
||||||
|
else if usernameLength > maxUsernameChars then
|
||||||
|
[ "Username too long" ]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
|
||||||
|
-}
|
||||||
|
fromErrors : (subject -> List error) -> Validator error subject
|
||||||
|
fromErrors toErrors =
|
||||||
|
Validator toErrors
|
||||||
|
|
||||||
|
|
||||||
{-| Return an error if a predicate returns `True` for the given
|
{-| Return an error if a predicate returns `True` for the given
|
||||||
subject.
|
subject.
|
||||||
|
|
||||||
import Validate exposing (ifTrue)
|
import Validate exposing (Validator, ifTrue)
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
modelValidator : Validator Model String
|
||||||
modelValidator =
|
modelValidator =
|
||||||
@@ -199,7 +270,7 @@ ifTrue test error =
|
|||||||
{-| Return an error if a predicate returns `False` for the given
|
{-| Return an error if a predicate returns `False` for the given
|
||||||
subject.
|
subject.
|
||||||
|
|
||||||
import Validate exposing (ifFalse)
|
import Validate exposing (Validator, ifFalse)
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
modelValidator : Validator Model String
|
||||||
modelValidator =
|
modelValidator =
|
||||||
@@ -226,7 +297,7 @@ ifFalse test error =
|
|||||||
{-| Run each of the given validators, in order, and return their concatenated
|
{-| Run each of the given validators, in order, and return their concatenated
|
||||||
error lists.
|
error lists.
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifNotInt)
|
import Validate exposing (Validator, ifBlank, ifNotInt)
|
||||||
|
|
||||||
modelValidator : Validator Model String
|
modelValidator : Validator Model String
|
||||||
modelValidator =
|
modelValidator =
|
||||||
@@ -253,7 +324,7 @@ all validators =
|
|||||||
{-| Run each of the given validators, in order, stopping after the first error
|
{-| Run each of the given validators, in order, stopping after the first error
|
||||||
and returning it. If no errors are encountered, return `Nothing`.
|
and returning it. If no errors are encountered, return `Nothing`.
|
||||||
|
|
||||||
import Validate exposing (ifBlank, ifInvalidEmail, ifNotInt)
|
import Validate exposing (Validator, ifBlank, ifInvalidEmail, ifNotInt)
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
@@ -337,7 +408,7 @@ isBlank str =
|
|||||||
Regex.contains lacksNonWhitespaceChars str
|
Regex.contains lacksNonWhitespaceChars str
|
||||||
|
|
||||||
|
|
||||||
{-| Returns `True` if the email is malformed.
|
{-| Returns `True` if the email is valid.
|
||||||
|
|
||||||
[`ifInvalidEmail`](#ifInvalidEmail) uses this under the hood.
|
[`ifInvalidEmail`](#ifInvalidEmail) uses this under the hood.
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ module Data.Article.Author exposing (Author, decoder)
|
|||||||
import Data.User as User exposing (Username)
|
import Data.User as User exposing (Username)
|
||||||
import Data.UserPhoto as UserPhoto exposing (UserPhoto)
|
import Data.UserPhoto as UserPhoto exposing (UserPhoto)
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
import Json.Decode as Decode exposing (Decoder)
|
||||||
import Json.Decode.Pipeline exposing (custom, decode, required)
|
import Json.Decode.Pipeline exposing (custom, decode, optional, required)
|
||||||
|
|
||||||
|
|
||||||
decoder : Decoder Author
|
decoder : Decoder Author
|
||||||
@@ -12,7 +12,7 @@ decoder =
|
|||||||
|> required "username" User.usernameDecoder
|
|> required "username" User.usernameDecoder
|
||||||
|> required "bio" (Decode.nullable Decode.string)
|
|> required "bio" (Decode.nullable Decode.string)
|
||||||
|> required "image" UserPhoto.decoder
|
|> required "image" UserPhoto.decoder
|
||||||
|> required "following" Decode.bool
|
|> optional "following" Decode.bool False
|
||||||
|
|
||||||
|
|
||||||
type alias Author =
|
type alias Author =
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Data.AuthToken as AuthToken exposing (AuthToken)
|
|||||||
import Data.UserPhoto as UserPhoto exposing (UserPhoto)
|
import Data.UserPhoto as UserPhoto exposing (UserPhoto)
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
import Json.Decode as Decode exposing (Decoder)
|
||||||
import Json.Decode.Pipeline exposing (decode, required)
|
import Json.Decode.Pipeline exposing (decode, optional, required)
|
||||||
import Json.Encode as Encode exposing (Value)
|
import Json.Encode as Encode exposing (Value)
|
||||||
import Json.Encode.Extra as EncodeExtra
|
import Json.Encode.Extra as EncodeExtra
|
||||||
import UrlParser
|
import UrlParser
|
||||||
@@ -16,8 +16,6 @@ type alias User =
|
|||||||
, username : Username
|
, username : Username
|
||||||
, bio : Maybe String
|
, bio : Maybe String
|
||||||
, image : UserPhoto
|
, image : UserPhoto
|
||||||
, createdAt : String
|
|
||||||
, updatedAt : String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -33,8 +31,6 @@ decoder =
|
|||||||
|> required "username" usernameDecoder
|
|> required "username" usernameDecoder
|
||||||
|> required "bio" (Decode.nullable Decode.string)
|
|> required "bio" (Decode.nullable Decode.string)
|
||||||
|> required "image" UserPhoto.decoder
|
|> required "image" UserPhoto.decoder
|
||||||
|> required "createdAt" Decode.string
|
|
||||||
|> required "updatedAt" Decode.string
|
|
||||||
|
|
||||||
|
|
||||||
encode : User -> Value
|
encode : User -> Value
|
||||||
@@ -45,8 +41,6 @@ encode user =
|
|||||||
, ( "username", encodeUsername user.username )
|
, ( "username", encodeUsername user.username )
|
||||||
, ( "bio", EncodeExtra.maybe Encode.string user.bio )
|
, ( "bio", EncodeExtra.maybe Encode.string user.bio )
|
||||||
, ( "image", UserPhoto.encode user.image )
|
, ( "image", UserPhoto.encode user.image )
|
||||||
, ( "createdAt", Encode.string user.createdAt )
|
|
||||||
, ( "updatedAt", Encode.string user.updatedAt )
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,15 @@ photoToUrl : UserPhoto -> String
|
|||||||
photoToUrl (UserPhoto maybeUrl) =
|
photoToUrl (UserPhoto maybeUrl) =
|
||||||
case maybeUrl of
|
case maybeUrl of
|
||||||
Nothing ->
|
Nothing ->
|
||||||
"https://static.productionready.io/images/smiley-cyrus.jpg"
|
defaultPhotoUrl
|
||||||
|
|
||||||
|
Just "" ->
|
||||||
|
defaultPhotoUrl
|
||||||
|
|
||||||
Just url ->
|
Just url ->
|
||||||
url
|
url
|
||||||
|
|
||||||
|
|
||||||
|
defaultPhotoUrl : String
|
||||||
|
defaultPhotoUrl =
|
||||||
|
"/assets/images/smiley-cyrus.jpg"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Data.Article as Article exposing (Article, Body)
|
|||||||
import Data.Session exposing (Session)
|
import Data.Session exposing (Session)
|
||||||
import Data.User exposing (User)
|
import Data.User exposing (User)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (attribute, class, disabled, href, id, placeholder, type_, value)
|
import Html.Attributes exposing (attribute, class, defaultValue, disabled, href, id, placeholder, type_)
|
||||||
import Html.Events exposing (onInput, onSubmit)
|
import Html.Events exposing (onInput, onSubmit)
|
||||||
import Http
|
import Http
|
||||||
import Page.Errored exposing (PageLoadError, pageLoadError)
|
import Page.Errored exposing (PageLoadError, pageLoadError)
|
||||||
@@ -102,26 +102,26 @@ viewForm model =
|
|||||||
[ class "form-control-lg"
|
[ class "form-control-lg"
|
||||||
, placeholder "Article Title"
|
, placeholder "Article Title"
|
||||||
, onInput SetTitle
|
, onInput SetTitle
|
||||||
, value model.title
|
, defaultValue model.title
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
, Form.input
|
, Form.input
|
||||||
[ placeholder "What's this article about?"
|
[ placeholder "What's this article about?"
|
||||||
, onInput SetDescription
|
, onInput SetDescription
|
||||||
, value model.description
|
, defaultValue model.description
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
, Form.textarea
|
, Form.textarea
|
||||||
[ placeholder "Write your article (in markdown)"
|
[ placeholder "Write your article (in markdown)"
|
||||||
, attribute "rows" "8"
|
, attribute "rows" "8"
|
||||||
, onInput SetBody
|
, onInput SetBody
|
||||||
, value model.body
|
, defaultValue model.body
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
, Form.input
|
, Form.input
|
||||||
[ placeholder "Enter tags"
|
[ placeholder "Enter tags"
|
||||||
, onInput SetTags
|
, onInput SetTags
|
||||||
, value (String.join " " model.tags)
|
, defaultValue (String.join " " model.tags)
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
, button [ class "btn btn-lg pull-xs-right btn-primary", disabled model.isSaving ]
|
, button [ class "btn btn-lg pull-xs-right btn-primary", disabled model.isSaving ]
|
||||||
|
|||||||
@@ -186,10 +186,23 @@ modelValidator =
|
|||||||
Validate.all
|
Validate.all
|
||||||
[ ifBlank .username ( Username, "username can't be blank." )
|
[ ifBlank .username ( Username, "username can't be blank." )
|
||||||
, ifBlank .email ( Email, "email can't be blank." )
|
, ifBlank .email ( Email, "email can't be blank." )
|
||||||
, ifBlank .password ( Password, "password can't be blank." )
|
, Validate.fromErrors passwordLength
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
minPasswordChars : Int
|
||||||
|
minPasswordChars =
|
||||||
|
6
|
||||||
|
|
||||||
|
|
||||||
|
passwordLength : Model -> List Error
|
||||||
|
passwordLength { password } =
|
||||||
|
if String.length password < minPasswordChars then
|
||||||
|
[ ( Password, "password must be at least " ++ toString minPasswordChars ++ " characters long." ) ]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
errorsDecoder : Decoder (List String)
|
errorsDecoder : Decoder (List String)
|
||||||
errorsDecoder =
|
errorsDecoder =
|
||||||
decode (\email username password -> List.concat [ email, username, password ])
|
decode (\email username password -> List.concat [ email, username, password ])
|
||||||
|
|||||||
@@ -7,12 +7,22 @@ import Data.Article exposing (Article)
|
|||||||
import Data.UserPhoto as UserPhoto exposing (UserPhoto)
|
import Data.UserPhoto as UserPhoto exposing (UserPhoto)
|
||||||
import Date.Format
|
import Date.Format
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (attribute, class, classList, href, id, src)
|
import Html.Attributes exposing (attribute, class, classList, href, id, placeholder, src)
|
||||||
import Route exposing (Route)
|
import Route exposing (Route)
|
||||||
import Views.Article.Favorite as Favorite
|
import Views.Article.Favorite as Favorite
|
||||||
import Views.Author
|
import Views.Author
|
||||||
|
|
||||||
|
|
||||||
|
-- VIEWS --
|
||||||
|
|
||||||
|
|
||||||
|
{-| Some pages want to view just the timestamp, not the whole article.
|
||||||
|
-}
|
||||||
|
viewTimestamp : Article a -> Html msg
|
||||||
|
viewTimestamp article =
|
||||||
|
span [ class "date" ] [ text (formattedTimestamp article) ]
|
||||||
|
|
||||||
|
|
||||||
view : (Article a -> msg) -> Article a -> Html msg
|
view : (Article a -> msg) -> Article a -> Html msg
|
||||||
view toggleFavorite article =
|
view toggleFavorite article =
|
||||||
let
|
let
|
||||||
@@ -25,7 +35,7 @@ view toggleFavorite article =
|
|||||||
[ img [ UserPhoto.src author.image ] [] ]
|
[ img [ UserPhoto.src author.image ] [] ]
|
||||||
, div [ class "info" ]
|
, div [ class "info" ]
|
||||||
[ Views.Author.view author.username
|
[ Views.Author.view author.username
|
||||||
, viewTimestamp article
|
, span [ class "date" ] [ text (formattedTimestamp article) ]
|
||||||
]
|
]
|
||||||
, Favorite.button
|
, Favorite.button
|
||||||
toggleFavorite
|
toggleFavorite
|
||||||
@@ -41,9 +51,8 @@ view toggleFavorite article =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
viewTimestamp : Article a -> Html msg
|
|
||||||
viewTimestamp article =
|
-- INTERNAL --
|
||||||
span [ class "date" ] [ text (formattedTimestamp article) ]
|
|
||||||
|
|
||||||
|
|
||||||
formattedTimestamp : Article a -> String
|
formattedTimestamp : Article a -> String
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module Data.Article.Author exposing (Author, decoder)
|
|||||||
import Data.User as User exposing (Username)
|
import Data.User as User exposing (Username)
|
||||||
import Data.UserPhoto as UserPhoto exposing (UserPhoto)
|
import Data.UserPhoto as UserPhoto exposing (UserPhoto)
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
import Json.Decode as Decode exposing (Decoder)
|
||||||
import Json.Decode.Pipeline exposing (custom, decode, required)
|
import Json.Decode.Pipeline exposing (custom, decode, optional, required)
|
||||||
|
|
||||||
|
|
||||||
decoder : Decoder Author
|
decoder : Decoder Author
|
||||||
@@ -12,7 +12,7 @@ decoder =
|
|||||||
|> required "username" User.usernameDecoder
|
|> required "username" User.usernameDecoder
|
||||||
|> required "bio" (Decode.nullable Decode.string)
|
|> required "bio" (Decode.nullable Decode.string)
|
||||||
|> required "image" UserPhoto.decoder
|
|> required "image" UserPhoto.decoder
|
||||||
|> required "following" Decode.bool
|
|> optional "following" Decode.bool False
|
||||||
|
|
||||||
|
|
||||||
type alias Author =
|
type alias Author =
|
||||||
|
|||||||
@@ -39,7 +39,15 @@ photoToUrl : UserPhoto -> String
|
|||||||
photoToUrl (UserPhoto maybeUrl) =
|
photoToUrl (UserPhoto maybeUrl) =
|
||||||
case maybeUrl of
|
case maybeUrl of
|
||||||
Nothing ->
|
Nothing ->
|
||||||
"https://static.productionready.io/images/smiley-cyrus.jpg"
|
defaultPhotoUrl
|
||||||
|
|
||||||
|
Just "" ->
|
||||||
|
defaultPhotoUrl
|
||||||
|
|
||||||
Just url ->
|
Just url ->
|
||||||
url
|
url
|
||||||
|
|
||||||
|
|
||||||
|
defaultPhotoUrl : String
|
||||||
|
defaultPhotoUrl =
|
||||||
|
"/assets/images/smiley-cyrus.jpg"
|
||||||
|
|||||||
@@ -238,14 +238,14 @@ modelValidator =
|
|||||||
-- INTERNAL --
|
-- INTERNAL --
|
||||||
|
|
||||||
|
|
||||||
redirectToArticle : Article.Slug -> Cmd msg
|
|
||||||
redirectToArticle =
|
|
||||||
Route.modifyUrl << Route.Article
|
|
||||||
|
|
||||||
|
|
||||||
tagsFromString : String -> List String
|
tagsFromString : String -> List String
|
||||||
tagsFromString str =
|
tagsFromString str =
|
||||||
str
|
str
|
||||||
|> String.split " "
|
|> String.split " "
|
||||||
|> List.map String.trim
|
|> List.map String.trim
|
||||||
|> List.filter (not << String.isEmpty)
|
|> List.filter (not << String.isEmpty)
|
||||||
|
|
||||||
|
|
||||||
|
redirectToArticle : Article.Slug -> Cmd msg
|
||||||
|
redirectToArticle =
|
||||||
|
Route.modifyUrl << Route.Article
|
||||||
|
|||||||
@@ -133,37 +133,12 @@ update session msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
Just user ->
|
Just user ->
|
||||||
let
|
user.token
|
||||||
-- TODO
|
|> Request.Profile.toggleFollow
|
||||||
-- 1) head over to the Request.Profile module and look up
|
profile.username
|
||||||
-- what arguments the `toggleFollow` function takes.
|
profile.following
|
||||||
--
|
|> Http.send FollowCompleted
|
||||||
-- 2) call Request.Profile.toggleFollow here,
|
|> pair model
|
||||||
-- to get back a Request.
|
|
||||||
--
|
|
||||||
-- 3) pass that Request to Http.send to get a Cmd.
|
|
||||||
-- Use that Cmd here.
|
|
||||||
--
|
|
||||||
-- Here's the documentation for Http.send:
|
|
||||||
-- http://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http#send
|
|
||||||
--
|
|
||||||
-- Here are some hepful values that are in scope:
|
|
||||||
--
|
|
||||||
-- user.token : Maybe AuthToken
|
|
||||||
--
|
|
||||||
-- profile : Profile [look in the Data.Profile module!]
|
|
||||||
--
|
|
||||||
-- FollowCompleted : Result Http.Error Profile -> Msg
|
|
||||||
--
|
|
||||||
cmd : Cmd Msg
|
|
||||||
cmd =
|
|
||||||
Request.Profile.toggleFollow
|
|
||||||
profile.username
|
|
||||||
profile.following
|
|
||||||
user.token
|
|
||||||
|> Http.send FollowCompleted
|
|
||||||
in
|
|
||||||
( model, cmd )
|
|
||||||
|
|
||||||
FollowCompleted (Ok newProfile) ->
|
FollowCompleted (Ok newProfile) ->
|
||||||
( { model | profile = newProfile }, Cmd.none )
|
( { model | profile = newProfile }, Cmd.none )
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
module RoutingTests exposing (..)
|
|
||||||
|
|
||||||
import Data.Article as Article exposing (Slug)
|
|
||||||
import Data.User as User exposing (Username)
|
|
||||||
import Expect exposing (Expectation)
|
|
||||||
import Json.Decode exposing (decodeString)
|
|
||||||
import Navigation exposing (Location)
|
|
||||||
import Route exposing (Route(..))
|
|
||||||
import Test exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
-- TODO need to add lots more tests!
|
|
||||||
|
|
||||||
|
|
||||||
fromLocation : Test
|
|
||||||
fromLocation =
|
|
||||||
describe "Route.fromLocation"
|
|
||||||
[ testLocation "" Root
|
|
||||||
, testLocation "#login" Login
|
|
||||||
, testLocation "#logout" Logout
|
|
||||||
, testLocation "#settings" Settings
|
|
||||||
, testLocation "#profile/foo" (Profile (usernameFromStr "foo"))
|
|
||||||
, testLocation "#register" Register
|
|
||||||
, testLocation "#article/foo" (Article (slugFromStr "foo"))
|
|
||||||
, testLocation "#editor" NewArticle
|
|
||||||
, testLocation "#editor/foo" (EditArticle (slugFromStr "foo"))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- HELPERS --
|
|
||||||
|
|
||||||
|
|
||||||
testLocation : String -> Route -> Test
|
|
||||||
testLocation hash route =
|
|
||||||
test ("Parsing hash: \"" ++ hash ++ "\"") <|
|
|
||||||
\() ->
|
|
||||||
makeHashLocation hash
|
|
||||||
|> Route.fromLocation
|
|
||||||
|> Expect.equal (Just route)
|
|
||||||
|
|
||||||
|
|
||||||
makeHashLocation : String -> Location
|
|
||||||
makeHashLocation hash =
|
|
||||||
{ hash = hash
|
|
||||||
, href = ""
|
|
||||||
, host = ""
|
|
||||||
, hostname = ""
|
|
||||||
, protocol = ""
|
|
||||||
, origin = ""
|
|
||||||
, port_ = ""
|
|
||||||
, pathname = ""
|
|
||||||
, search = ""
|
|
||||||
, username = ""
|
|
||||||
, password = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- CONSTRUCTING UNEXPOSED VALUES --
|
|
||||||
-- By decoding values that are not intended to be exposed directly - and crashing
|
|
||||||
-- if they cannot be decoded, since crashing is harmless in tests - we can let
|
|
||||||
-- our internal modules continue to expose only the intended ways of
|
|
||||||
-- constructing those, while still being able to test them.
|
|
||||||
|
|
||||||
|
|
||||||
usernameFromStr : String -> Username
|
|
||||||
usernameFromStr str =
|
|
||||||
case decodeString User.usernameDecoder ("\"" ++ str ++ "\"") of
|
|
||||||
Ok username ->
|
|
||||||
username
|
|
||||||
|
|
||||||
Err err ->
|
|
||||||
Debug.crash ("Error decoding Username from \"" ++ str ++ "\": " ++ err)
|
|
||||||
|
|
||||||
|
|
||||||
slugFromStr : String -> Slug
|
|
||||||
slugFromStr str =
|
|
||||||
let
|
|
||||||
json =
|
|
||||||
"""
|
|
||||||
{ "description": null
|
|
||||||
, "slug": \"""" ++ str ++ """"
|
|
||||||
, "title": ""
|
|
||||||
, "tagList": []
|
|
||||||
, "createdAt": "2012-04-23T18:25:43.511Z"
|
|
||||||
, "updatedAt": "2012-04-23T18:25:43.511Z"
|
|
||||||
, "favorited": false
|
|
||||||
, "favoritesCount": 1
|
|
||||||
, "author":
|
|
||||||
{ "username": ""
|
|
||||||
, "bio": null
|
|
||||||
, "image": null
|
|
||||||
, "following": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
in
|
|
||||||
case decodeString Article.decoder json of
|
|
||||||
Ok article ->
|
|
||||||
article.slug
|
|
||||||
|
|
||||||
Err err ->
|
|
||||||
Debug.crash ("Error decoding Slug from \"" ++ str ++ "\": " ++ err)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "Test Suites",
|
|
||||||
"repository": "https://github.com/user/project.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"../src",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"dependencies": {
|
|
||||||
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
|
|
||||||
"eeue56/elm-html-test": "5.1.2 <= v < 6.0.0",
|
|
||||||
"elm-community/elm-test": "4.0.0 <= v < 5.0.0",
|
|
||||||
"elm-community/json-extra": "2.1.0 <= v < 3.0.0",
|
|
||||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
|
||||||
"elm-lang/dom": "1.1.1 <= v < 2.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
|
||||||
"elm-lang/http": "1.0.0 <= v < 2.0.0",
|
|
||||||
"elm-lang/navigation": "2.1.0 <= v < 3.0.0",
|
|
||||||
"evancz/elm-markdown": "3.0.2 <= v < 4.0.0",
|
|
||||||
"evancz/url-parser": "2.0.1 <= v < 3.0.0",
|
|
||||||
"lukewestby/elm-http-builder": "5.1.0 <= v < 6.0.0",
|
|
||||||
"mgold/elm-date-format": "1.3.0 <= v < 2.0.0",
|
|
||||||
"rtfeldman/elm-validate": "2.0.0 <= v < 3.0.0",
|
|
||||||
"rtfeldman/selectlist": "1.0.0 <= v < 2.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"rtfeldman/elm-validate": "2.0.0",
|
|
||||||
"eeue56/elm-html-query": "3.0.0",
|
|
||||||
"rtfeldman/selectlist": "1.0.0",
|
|
||||||
"elm-community/elm-test": "4.2.0",
|
|
||||||
"elm-lang/navigation": "2.1.0",
|
|
||||||
"elm-lang/virtual-dom": "2.0.4",
|
|
||||||
"eeue56/elm-lazy": "1.0.0",
|
|
||||||
"evancz/url-parser": "2.0.1",
|
|
||||||
"mgold/elm-random-pcg": "5.0.2",
|
|
||||||
"mgold/elm-date-format": "1.4.2",
|
|
||||||
"evancz/elm-markdown": "3.0.2",
|
|
||||||
"eeue56/elm-lazy-list": "1.0.0",
|
|
||||||
"elm-lang/dom": "1.1.1",
|
|
||||||
"elm-lang/html": "2.0.0",
|
|
||||||
"elm-community/json-extra": "2.6.0",
|
|
||||||
"elm-lang/http": "1.0.0",
|
|
||||||
"eeue56/elm-shrink": "1.0.0",
|
|
||||||
"lukewestby/elm-http-builder": "5.1.0",
|
|
||||||
"eeue56/elm-html-in-elm": "5.2.0",
|
|
||||||
"eeue56/elm-html-test": "5.1.2",
|
|
||||||
"NoRedInk/elm-decode-pipeline": "3.0.0",
|
|
||||||
"elm-lang/core": "5.1.1"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# elm-package generated files
|
|
||||||
elm-stuff/
|
|
||||||
# elm-repl generated files
|
|
||||||
repl-temp-*
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
Copyright (c) 2016, NoRedInk
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of elm-decode-pipeline nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
# elm-decode-pipeline
|
|
||||||
|
|
||||||
A library for building decoders using the pipeline [`(|>)`](http://package.elm-lang.org/packages/elm-lang/core/3.0.0/Basics#|>)
|
|
||||||
operator and plain function calls.
|
|
||||||
|
|
||||||
## Motivation
|
|
||||||
|
|
||||||
It's common to decode into a record that has a `type alias`. Here's an example
|
|
||||||
of this from the [`object3`](http://package.elm-lang.org/packages/elm-lang/core/3.0.0/Json-Decode#object3)
|
|
||||||
docs:
|
|
||||||
|
|
||||||
```elm
|
|
||||||
type alias Job = { name : String, id : Int, completed : Bool }
|
|
||||||
|
|
||||||
point : Decoder Job
|
|
||||||
point =
|
|
||||||
object3 Job
|
|
||||||
("name" := string)
|
|
||||||
("id" := int)
|
|
||||||
("completed" := bool)
|
|
||||||
```
|
|
||||||
|
|
||||||
This works because a record type alias can be called as a normal function. In
|
|
||||||
that case it accepts one argument for each field (in whatever order the fields
|
|
||||||
are declared in the type alias) and then returns an appropriate record built
|
|
||||||
with those arguments.
|
|
||||||
|
|
||||||
The `objectN` decoders are straightforward, but require manually changing N
|
|
||||||
whenever the field count changes. This library provides functions designed to
|
|
||||||
be used with the `|>` operator, with the goal of having decoders that are both
|
|
||||||
easy to read and easy to modify.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Here is a decoder built with this library.
|
|
||||||
|
|
||||||
```elm
|
|
||||||
import Json.Decode exposing (int, string, float, Decoder)
|
|
||||||
import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
|
|
||||||
|
|
||||||
|
|
||||||
type alias User =
|
|
||||||
{ id : Int
|
|
||||||
, email : Maybe String
|
|
||||||
, name : String
|
|
||||||
, percentExcited : Float
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userDecoder : Decoder User
|
|
||||||
userDecoder =
|
|
||||||
decode User
|
|
||||||
|> required "id" int
|
|
||||||
|> required "email" (nullable string) -- `null` decodes to `Nothing`
|
|
||||||
|> optional "name" string "(fallback if name is `null` or not present)"
|
|
||||||
|> hardcoded 1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example:
|
|
||||||
|
|
||||||
* `decode` is a synonym for [`succeed`](http://package.elm-lang.org/packages/elm-lang/core/3.0.0/Json-Decode#succeed) (it just reads better here)
|
|
||||||
* `required "id" int` is similar to `("id" := int)`
|
|
||||||
* `optional` is like `required`, but if the field is either `null` or not present, decoding does not fail; instead it succeeds with the provided fallback value.
|
|
||||||
* `hardcoded` does not look at the provided JSON, and instead always decodes to the same value.
|
|
||||||
|
|
||||||
You could use this decoder as follows:
|
|
||||||
|
|
||||||
```elm
|
|
||||||
Json.Decode.decodeString
|
|
||||||
userDecoder
|
|
||||||
"""
|
|
||||||
{"id": 123, "email": "sam@example.com", "name": "Sam Sample"}
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
The result would be:
|
|
||||||
|
|
||||||
```elm
|
|
||||||
{ id = 123
|
|
||||||
, email = "sam@example.com"
|
|
||||||
, name = "Sam Sample"
|
|
||||||
, percentExcited = 1.0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, you could use it like so:
|
|
||||||
|
|
||||||
```elm
|
|
||||||
Json.Decode.decodeString
|
|
||||||
userDecoder
|
|
||||||
"""
|
|
||||||
{"id": 123, "email": "sam@example.com", "percentExcited": "(hardcoded)"}
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
In this case, the result would be:
|
|
||||||
|
|
||||||
```elm
|
|
||||||
{ id = 123
|
|
||||||
, email = "sam@example.com"
|
|
||||||
, name = "(fallback if name not present)"
|
|
||||||
, percentExcited = 1.0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
[][team]
|
|
||||||
[team]: http://noredink.com/about/team
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "3.0.0",
|
|
||||||
"summary": "A pipeline-friendly library for building JSON decoders.",
|
|
||||||
"repository": "https://github.com/NoRedInk/elm-decode-pipeline.git",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"source-directories": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [
|
|
||||||
"Json.Decode.Pipeline"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
module Example exposing (..)
|
|
||||||
|
|
||||||
import Json.Decode exposing (int, string, float, Decoder)
|
|
||||||
import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
|
|
||||||
|
|
||||||
|
|
||||||
type alias User =
|
|
||||||
{ id : Int
|
|
||||||
, name : String
|
|
||||||
, percentExcited : Float
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userDecoder : Decoder User
|
|
||||||
userDecoder =
|
|
||||||
decode User
|
|
||||||
|> required "id" int
|
|
||||||
|> optional "name" string "(fallback if name not present)"
|
|
||||||
|> hardcoded 1.0
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
module Json.Decode.Pipeline exposing (required, requiredAt, optional, optionalAt, resolve, decode, hardcoded, custom)
|
|
||||||
|
|
||||||
{-| # Json.Decode.Pipeline
|
|
||||||
|
|
||||||
Use the `(|>)` operator to build JSON decoders.
|
|
||||||
|
|
||||||
## Decoding fields
|
|
||||||
|
|
||||||
@docs required, requiredAt, optional, optionalAt, hardcoded, custom
|
|
||||||
|
|
||||||
## Beginning and ending pipelines
|
|
||||||
|
|
||||||
@docs decode, resolve
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Decode a required field.
|
|
||||||
|
|
||||||
import Json.Decode exposing (int, string, Decoder)
|
|
||||||
import Decode.Pipeline exposing (decode, required)
|
|
||||||
|
|
||||||
|
|
||||||
type alias User =
|
|
||||||
{ id : Int
|
|
||||||
, name : String
|
|
||||||
, email : String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userDecoder : Decoder User
|
|
||||||
userDecoder =
|
|
||||||
decode User
|
|
||||||
|> required "id" int
|
|
||||||
|> required "name" string
|
|
||||||
|> required "email" string
|
|
||||||
|
|
||||||
|
|
||||||
result : Result String User
|
|
||||||
result =
|
|
||||||
Decode.decodeString
|
|
||||||
userDecoder
|
|
||||||
"""
|
|
||||||
{"id": 123, "email": "sam@example.com", "name": "Sam"}
|
|
||||||
"""
|
|
||||||
-- Ok { id = 123, name = "Sam", email = "sam@example.com" }
|
|
||||||
-}
|
|
||||||
required : String -> Decoder a -> Decoder (a -> b) -> Decoder b
|
|
||||||
required key valDecoder decoder =
|
|
||||||
custom (Decode.field key valDecoder) decoder
|
|
||||||
|
|
||||||
|
|
||||||
{-| Decode a required nested field.
|
|
||||||
-}
|
|
||||||
requiredAt : List String -> Decoder a -> Decoder (a -> b) -> Decoder b
|
|
||||||
requiredAt path valDecoder decoder =
|
|
||||||
custom (Decode.at path valDecoder) decoder
|
|
||||||
|
|
||||||
|
|
||||||
{-| Decode a field that may be missing or have a null value. If the field is
|
|
||||||
missing, then it decodes as the `fallback` value. If the field is present,
|
|
||||||
then `valDecoder` is used to decode its value. If `valDecoder` fails on a
|
|
||||||
`null` value, then the `fallback` is used as if the field were missing
|
|
||||||
entirely.
|
|
||||||
|
|
||||||
import Json.Decode exposing (int, string, null, oneOf, Decoder)
|
|
||||||
import Decode.Pipeline exposing (decode, required, optional)
|
|
||||||
|
|
||||||
|
|
||||||
type alias User =
|
|
||||||
{ id : Int
|
|
||||||
, name : String
|
|
||||||
, email : String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userDecoder : Decoder User
|
|
||||||
userDecoder =
|
|
||||||
decode User
|
|
||||||
|> required "id" int
|
|
||||||
|> optional "name" string "blah"
|
|
||||||
|> required "email" string
|
|
||||||
|
|
||||||
|
|
||||||
result : Result String User
|
|
||||||
result =
|
|
||||||
Decode.decodeString
|
|
||||||
userDecoder
|
|
||||||
"""
|
|
||||||
{"id": 123, "email": "sam@example.com" }
|
|
||||||
"""
|
|
||||||
-- Ok { id = 123, name = "blah", email = "sam@example.com" }
|
|
||||||
|
|
||||||
Because `valDecoder` is given an opportunity to decode `null` values before
|
|
||||||
resorting to the `fallback`, you can distinguish between missing and `null`
|
|
||||||
values if you need to:
|
|
||||||
|
|
||||||
userDecoder2 =
|
|
||||||
decode User
|
|
||||||
|> required "id" int
|
|
||||||
|> optional "name" (oneOf [ string, null "NULL" ]) "MISSING"
|
|
||||||
|> required "email" string
|
|
||||||
|
|
||||||
-}
|
|
||||||
optional : String -> Decoder a -> a -> Decoder (a -> b) -> Decoder b
|
|
||||||
optional key valDecoder fallback decoder =
|
|
||||||
custom (optionalDecoder (Decode.field key Decode.value) valDecoder fallback) decoder
|
|
||||||
|
|
||||||
|
|
||||||
{-| Decode an optional nested field.
|
|
||||||
-}
|
|
||||||
optionalAt : List String -> Decoder a -> a -> Decoder (a -> b) -> Decoder b
|
|
||||||
optionalAt path valDecoder fallback decoder =
|
|
||||||
custom (optionalDecoder (Decode.at path Decode.value) valDecoder fallback) decoder
|
|
||||||
|
|
||||||
|
|
||||||
optionalDecoder : Decoder Decode.Value -> Decoder a -> a -> Decoder a
|
|
||||||
optionalDecoder pathDecoder valDecoder fallback =
|
|
||||||
let
|
|
||||||
nullOr decoder =
|
|
||||||
Decode.oneOf [ decoder, Decode.null fallback ]
|
|
||||||
|
|
||||||
handleResult input =
|
|
||||||
case Decode.decodeValue pathDecoder input of
|
|
||||||
Ok rawValue ->
|
|
||||||
-- The field was present, so now let's try to decode that value.
|
|
||||||
-- (If it was present but fails to decode, this should and will fail!)
|
|
||||||
case Decode.decodeValue (nullOr valDecoder) rawValue of
|
|
||||||
Ok finalResult ->
|
|
||||||
Decode.succeed finalResult
|
|
||||||
|
|
||||||
Err finalErr ->
|
|
||||||
Decode.fail finalErr
|
|
||||||
|
|
||||||
Err _ ->
|
|
||||||
-- The field was not present, so use the fallback.
|
|
||||||
Decode.succeed fallback
|
|
||||||
in
|
|
||||||
Decode.value
|
|
||||||
|> Decode.andThen handleResult
|
|
||||||
|
|
||||||
|
|
||||||
{-| Rather than decoding anything, use a fixed value for the next step in the
|
|
||||||
pipeline. `harcoded` does not look at the JSON at all.
|
|
||||||
|
|
||||||
import Json.Decode exposing (int, string, Decoder)
|
|
||||||
import Decode.Pipeline exposing (decode, required)
|
|
||||||
|
|
||||||
|
|
||||||
type alias User =
|
|
||||||
{ id : Int
|
|
||||||
, email : String
|
|
||||||
, followers : Int
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userDecoder : Decoder User
|
|
||||||
userDecoder =
|
|
||||||
decode User
|
|
||||||
|> required "id" int
|
|
||||||
|> required "email" string
|
|
||||||
|> hardcoded 0
|
|
||||||
|
|
||||||
|
|
||||||
result : Result String User
|
|
||||||
result =
|
|
||||||
Decode.decodeString
|
|
||||||
userDecoder
|
|
||||||
"""
|
|
||||||
{"id": 123, "email": "sam@example.com"}
|
|
||||||
"""
|
|
||||||
-- Ok { id = 123, email = "sam@example.com", followers = 0 }
|
|
||||||
-}
|
|
||||||
hardcoded : a -> Decoder (a -> b) -> Decoder b
|
|
||||||
hardcoded =
|
|
||||||
Decode.succeed >> custom
|
|
||||||
|
|
||||||
|
|
||||||
{-| Run the given decoder and feed its result into the pipeline at this point.
|
|
||||||
|
|
||||||
Consider this example.
|
|
||||||
|
|
||||||
import Json.Decode exposing (int, string, at, Decoder)
|
|
||||||
import Decode.Pipeline exposing (decode, required, custom)
|
|
||||||
|
|
||||||
|
|
||||||
type alias User =
|
|
||||||
{ id : Int
|
|
||||||
, name : String
|
|
||||||
, email : String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userDecoder : Decoder User
|
|
||||||
userDecoder =
|
|
||||||
decode User
|
|
||||||
|> required "id" int
|
|
||||||
|> custom (at [ "profile", "name" ] string)
|
|
||||||
|> required "email" string
|
|
||||||
|
|
||||||
|
|
||||||
result : Result String User
|
|
||||||
result =
|
|
||||||
Decode.decodeString
|
|
||||||
userDecoder
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"email": "sam@example.com",
|
|
||||||
"profile": {"name": "Sam"}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
-- Ok { id = 123, name = "Sam", email = "sam@example.com" }
|
|
||||||
-}
|
|
||||||
custom : Decoder a -> Decoder (a -> b) -> Decoder b
|
|
||||||
custom =
|
|
||||||
Decode.map2 (|>)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a `Decoder (Result x a)` into a `Decoder a`. Useful when you want
|
|
||||||
to perform some custom processing just before completing the decoding operation.
|
|
||||||
|
|
||||||
import Json.Decode exposing (int, string, float, Decoder)
|
|
||||||
import Decode.Pipeline exposing
|
|
||||||
(decode, required, resolve)
|
|
||||||
|
|
||||||
|
|
||||||
type alias User =
|
|
||||||
{ id : Int
|
|
||||||
, email : String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userDecoder : Decoder User
|
|
||||||
userDecoder =
|
|
||||||
let
|
|
||||||
-- toDecoder gets run *after* all the
|
|
||||||
-- (|> required ...) steps are done.
|
|
||||||
toDecoder : Int -> String -> Int -> Decoder User
|
|
||||||
toDecoder id email version =
|
|
||||||
if version > 2 then
|
|
||||||
succeed (User id email)
|
|
||||||
else
|
|
||||||
fail "This JSON is from a deprecated source. Please upgrade!"
|
|
||||||
in
|
|
||||||
decode toDecoder
|
|
||||||
|> required "id" int
|
|
||||||
|> required "email" string
|
|
||||||
|> required "version" int -- version is part of toDecoder,
|
|
||||||
|> resolve -- but it is not a part of User
|
|
||||||
|
|
||||||
|
|
||||||
result : Result String User
|
|
||||||
result =
|
|
||||||
Decode.decodeString
|
|
||||||
userDecoder
|
|
||||||
"""
|
|
||||||
{"id": 123, "email": "sam@example.com", "version": 1}
|
|
||||||
"""
|
|
||||||
-- Err "This JSON is from a deprecated source. Please upgrade!"
|
|
||||||
-}
|
|
||||||
resolve : Decoder (Decoder a) -> Decoder a
|
|
||||||
resolve =
|
|
||||||
Decode.andThen identity
|
|
||||||
|
|
||||||
|
|
||||||
{-| Begin a decoding pipeline. This is a synonym for [Json.Decode.succeed](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#succeed),
|
|
||||||
intended to make things read more clearly.
|
|
||||||
|
|
||||||
import Json.Decode exposing (int, string, float, Decoder)
|
|
||||||
import Json.Decode.Pipeline exposing (decode, required, optional)
|
|
||||||
|
|
||||||
|
|
||||||
type alias User =
|
|
||||||
{ id : Int
|
|
||||||
, email : String
|
|
||||||
, name : String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userDecoder : Decoder User
|
|
||||||
userDecoder =
|
|
||||||
decode User
|
|
||||||
|> required "id" int
|
|
||||||
|> required "email" string
|
|
||||||
|> optional "name" string ""
|
|
||||||
-}
|
|
||||||
decode : a -> Decoder a
|
|
||||||
decode =
|
|
||||||
Decode.succeed
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/elm-stuff/
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
port module Main exposing (..)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Run the tests with node-test-runner:
|
|
||||||
|
|
||||||
https://github.com/rtfeldman/node-test-runner
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Tests
|
|
||||||
import Test.Runner.Node exposing (run)
|
|
||||||
import Json.Encode exposing (Value)
|
|
||||||
|
|
||||||
|
|
||||||
main : Program Never
|
|
||||||
main =
|
|
||||||
run emit Tests.all
|
|
||||||
|
|
||||||
|
|
||||||
port emit : ( String, Value ) -> Cmd msg
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
module Tests exposing (..)
|
|
||||||
|
|
||||||
import Test exposing (..)
|
|
||||||
import Expect exposing (Expectation)
|
|
||||||
import Json.Decode.Pipeline
|
|
||||||
exposing
|
|
||||||
( decode
|
|
||||||
, required
|
|
||||||
, requiredAt
|
|
||||||
, optional
|
|
||||||
, optionalAt
|
|
||||||
, resolveResult
|
|
||||||
)
|
|
||||||
import Json.Decode exposing (Decoder, string, null)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Run some JSON through a Decoder and return the result.
|
|
||||||
-}
|
|
||||||
runWith : String -> Decoder a -> Result String a
|
|
||||||
runWith =
|
|
||||||
flip Json.Decode.decodeString
|
|
||||||
|
|
||||||
|
|
||||||
isError : Result err ok -> Bool
|
|
||||||
isError result =
|
|
||||||
case result of
|
|
||||||
Err _ ->
|
|
||||||
True
|
|
||||||
|
|
||||||
Ok _ ->
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
expectErr : Result err ok -> Expectation
|
|
||||||
expectErr result =
|
|
||||||
isError result
|
|
||||||
|> Expect.true ("Expected an Err but got " ++ toString result)
|
|
||||||
|
|
||||||
|
|
||||||
all : Test
|
|
||||||
all =
|
|
||||||
describe
|
|
||||||
"Json.Decode.Pipeline"
|
|
||||||
[ test "should decode basic example" <|
|
|
||||||
\() ->
|
|
||||||
decode (,)
|
|
||||||
|> required "a" string
|
|
||||||
|> required "b" string
|
|
||||||
|> runWith """{"a":"foo","b":"bar"}"""
|
|
||||||
|> Expect.equal (Ok ( "foo", "bar" ))
|
|
||||||
, test "should decode requiredAt fields" <|
|
|
||||||
\() ->
|
|
||||||
decode (,)
|
|
||||||
|> requiredAt [ "a" ] string
|
|
||||||
|> requiredAt [ "b", "c" ] string
|
|
||||||
|> runWith """{"a":"foo","b":{"c":"bar"}}"""
|
|
||||||
|> Expect.equal (Ok ( "foo", "bar" ))
|
|
||||||
, test "should decode optionalAt fields" <|
|
|
||||||
\() ->
|
|
||||||
decode (,)
|
|
||||||
|> optionalAt [ "a", "b" ] string "--"
|
|
||||||
|> optionalAt [ "x", "y" ] string "--"
|
|
||||||
|> runWith """{"a":{},"x":{"y":"bar"}}"""
|
|
||||||
|> Expect.equal (Ok ( "--", "bar" ))
|
|
||||||
, test "optional succeeds if the field is not present" <|
|
|
||||||
\() ->
|
|
||||||
decode (,)
|
|
||||||
|> optional "a" string "--"
|
|
||||||
|> optional "x" string "--"
|
|
||||||
|> runWith """{"x":"five"}"""
|
|
||||||
|> Expect.equal (Ok ( "--", "five" ))
|
|
||||||
, test "optional succeeds with fallback if the field is present but null" <|
|
|
||||||
\() ->
|
|
||||||
decode (,)
|
|
||||||
|> optional "a" string "--"
|
|
||||||
|> optional "x" string "--"
|
|
||||||
|> runWith """{"a":null,"x":"five"}"""
|
|
||||||
|> Expect.equal (Ok ( "--", "five" ))
|
|
||||||
, test "optional succeeds with result of the given decoder if the field is null and the decoder decodes nulls" <|
|
|
||||||
\() ->
|
|
||||||
decode (,)
|
|
||||||
|> optional "a" (null "null") "--"
|
|
||||||
|> optional "x" string "--"
|
|
||||||
|> runWith """{"a":null,"x":"five"}"""
|
|
||||||
|> Expect.equal (Ok ( "null", "five" ))
|
|
||||||
, test "optional fails if the field is present but doesn't decode" <|
|
|
||||||
\() ->
|
|
||||||
decode (,)
|
|
||||||
|> optional "a" string "--"
|
|
||||||
|> optional "x" string "--"
|
|
||||||
|> runWith """{"x":5}"""
|
|
||||||
|> expectErr
|
|
||||||
, test "optionalAt fails if the field is present but doesn't decode" <|
|
|
||||||
\() ->
|
|
||||||
decode (,)
|
|
||||||
|> optionalAt [ "a", "b" ] string "--"
|
|
||||||
|> optionalAt [ "x", "y" ] string "--"
|
|
||||||
|> runWith """{"a":{},"x":{"y":5}}"""
|
|
||||||
|> expectErr
|
|
||||||
, test "resolveResult bubbles up decoded Err results" <|
|
|
||||||
\() ->
|
|
||||||
decode Err
|
|
||||||
|> required "error" string
|
|
||||||
|> resolveResult
|
|
||||||
|> runWith """{"error":"invalid"}"""
|
|
||||||
|> expectErr
|
|
||||||
, test "resolveResult bubbles up decoded Ok results" <|
|
|
||||||
\() ->
|
|
||||||
decode Ok
|
|
||||||
|> required "ok" string
|
|
||||||
|> resolveResult
|
|
||||||
|> runWith """{"ok":"valid"}"""
|
|
||||||
|> Expect.equal (Ok "valid")
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "Sample Elm Test",
|
|
||||||
"repository": "https://github.com/user/project.git",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"source-directories": [
|
|
||||||
".",
|
|
||||||
"../src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"dependencies": {
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
|
||||||
"elm-community/elm-test": "2.0.0 <= v < 3.0.0",
|
|
||||||
"rtfeldman/node-test-runner": "1.0.0 <= v < 2.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# elm-package generated files
|
|
||||||
elm-stuff/
|
|
||||||
# elm-repl generated files
|
|
||||||
repl-temp-*
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- tests/elm-stuff/build-artifacts
|
|
||||||
|
|
||||||
os:
|
|
||||||
- osx
|
|
||||||
- linux
|
|
||||||
|
|
||||||
env:
|
|
||||||
matrix:
|
|
||||||
- ELM_VERSION=0.18 TARGET_NODE_VERSION=node
|
|
||||||
- ELM_VERSION=0.18 TARGET_NODE_VERSION=4.2
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- if [ ${TRAVIS_OS_NAME} == "osx" ];
|
|
||||||
then brew update; brew install nvm; mkdir ~/.nvm; export NVM_DIR=~/.nvm; source $(brew --prefix nvm)/nvm.sh;
|
|
||||||
fi
|
|
||||||
|
|
||||||
install:
|
|
||||||
- nvm install $TARGET_NODE_VERSION
|
|
||||||
- nvm use $TARGET_NODE_VERSION
|
|
||||||
- node --version
|
|
||||||
- npm --version
|
|
||||||
- npm install -g elm@$ELM_VERSION
|
|
||||||
- npm install -g elm-test
|
|
||||||
|
|
||||||
|
|
||||||
script:
|
|
||||||
- elm-test
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2016, Noah
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# elm-html-in-elm
|
|
||||||
|
|
||||||
|
|
||||||
A pure Elm represention of Elm Html. This module has been taken from [elm-server-side-renderer](https://github.com/eeue56/elm-server-side-renderer) and is a pure representation of the Html structure used by VirtualDom. It is designed to allow you to inspect Html nodes
|
|
||||||
|
|
||||||
This package is used to support testing with [elm-html-test](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest).
|
|
||||||
|
|
||||||
This package is also used to support using Elm as to generate static files for
|
|
||||||
your site with [elm-static-html](https://github.com/eeue56/elm-static-html)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5.2.0",
|
|
||||||
"summary": "A pure Elm representation of Elm Html",
|
|
||||||
"repository": "https://github.com/eeue56/elm-html-in-elm.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [
|
|
||||||
"ElmHtml.Constants",
|
|
||||||
"ElmHtml.InternalTypes",
|
|
||||||
"ElmHtml.Markdown",
|
|
||||||
"ElmHtml.ToString",
|
|
||||||
"ElmHtml.ToElmString",
|
|
||||||
"ElmHtml.ToHtml"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
module ElmHtml.Constants exposing (..)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Constants for representing internal keys for Elm's vdom implementation
|
|
||||||
|
|
||||||
@docs styleKey, eventKey, attributeKey, attributeNamespaceKey, knownKeys
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
|
|
||||||
{-| Internal key for style
|
|
||||||
-}
|
|
||||||
styleKey : String
|
|
||||||
styleKey =
|
|
||||||
"STYLE"
|
|
||||||
|
|
||||||
|
|
||||||
{-| Internal key for style
|
|
||||||
-}
|
|
||||||
eventKey : String
|
|
||||||
eventKey =
|
|
||||||
"EVENT"
|
|
||||||
|
|
||||||
|
|
||||||
{-| Internal key for style
|
|
||||||
-}
|
|
||||||
attributeKey : String
|
|
||||||
attributeKey =
|
|
||||||
"ATTR"
|
|
||||||
|
|
||||||
|
|
||||||
{-| Internal key for style
|
|
||||||
-}
|
|
||||||
attributeNamespaceKey : String
|
|
||||||
attributeNamespaceKey =
|
|
||||||
"ATTR_NS"
|
|
||||||
|
|
||||||
|
|
||||||
{-| Keys that we are aware of and should pay attention to
|
|
||||||
-}
|
|
||||||
knownKeys : List String
|
|
||||||
knownKeys =
|
|
||||||
[ styleKey, eventKey, attributeKey, attributeNamespaceKey ]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
module ElmHtml.Helpers exposing (..)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Internal helpers for ElmHtml
|
|
||||||
|
|
||||||
@docs filterKnownKeys
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Dict exposing (Dict)
|
|
||||||
import ElmHtml.Constants exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Filter out keys that we don't know
|
|
||||||
-}
|
|
||||||
filterKnownKeys : Dict String a -> Dict String a
|
|
||||||
filterKnownKeys =
|
|
||||||
Dict.filter (\key _ -> not (List.member key knownKeys))
|
|
||||||
@@ -1,581 +0,0 @@
|
|||||||
module ElmHtml.InternalTypes
|
|
||||||
exposing
|
|
||||||
( Attribute(..)
|
|
||||||
, AttributeRecord
|
|
||||||
, CustomNodeRecord
|
|
||||||
, ElementKind(..)
|
|
||||||
, ElmHtml(..)
|
|
||||||
, EventHandler
|
|
||||||
, EventRecord
|
|
||||||
, Facts
|
|
||||||
, MarkdownNodeRecord
|
|
||||||
, NamespacedAttributeRecord
|
|
||||||
, NodeRecord
|
|
||||||
, PropertyRecord
|
|
||||||
, Tagger
|
|
||||||
, TextTagRecord
|
|
||||||
, decodeAttribute
|
|
||||||
, decodeElmHtml
|
|
||||||
, emptyFacts
|
|
||||||
, toElementKind
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| Internal types used to represent Elm Html in pure Elm
|
|
||||||
|
|
||||||
@docs ElmHtml, TextTagRecord, NodeRecord, CustomNodeRecord, MarkdownNodeRecord
|
|
||||||
|
|
||||||
@docs Facts, Tagger, EventHandler, ElementKind
|
|
||||||
|
|
||||||
@docs Attribute, AttributeRecord, NamespacedAttributeRecord, PropertyRecord, EventRecord
|
|
||||||
|
|
||||||
@docs decodeElmHtml, emptyFacts, toElementKind, decodeAttribute
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Dict exposing (Dict)
|
|
||||||
import ElmHtml.Constants exposing (..)
|
|
||||||
import ElmHtml.Helpers exposing (..)
|
|
||||||
import ElmHtml.Markdown exposing (..)
|
|
||||||
import Html.Events
|
|
||||||
import Json.Decode exposing (field)
|
|
||||||
import Json.Encode
|
|
||||||
|
|
||||||
|
|
||||||
{-| Type tree for representing Elm's Html
|
|
||||||
|
|
||||||
- TextTag is just a plain old bit of text.
|
|
||||||
- NodeEntry is an actual HTML node, e.g a div
|
|
||||||
- CustomNode are nodes defined to work with the renderer in some way, e.g webgl/markdown
|
|
||||||
- MarkdownNode is just a wrapper for CustomNode designed just for markdown
|
|
||||||
|
|
||||||
-}
|
|
||||||
type ElmHtml msg
|
|
||||||
= TextTag TextTagRecord
|
|
||||||
| NodeEntry (NodeRecord msg)
|
|
||||||
| CustomNode (CustomNodeRecord msg)
|
|
||||||
| MarkdownNode (MarkdownNodeRecord msg)
|
|
||||||
| NoOp
|
|
||||||
|
|
||||||
|
|
||||||
{-| Text tags just contain text
|
|
||||||
-}
|
|
||||||
type alias TextTagRecord =
|
|
||||||
{ text : String }
|
|
||||||
|
|
||||||
|
|
||||||
{-| A node contains the `tag` as a string, the children, the facts (e.g attributes) and descendantsCount
|
|
||||||
-}
|
|
||||||
type alias NodeRecord msg =
|
|
||||||
{ tag : String
|
|
||||||
, children : List (ElmHtml msg)
|
|
||||||
, facts :
|
|
||||||
Facts msg
|
|
||||||
|
|
||||||
--, namespace : String
|
|
||||||
, descendantsCount : Int
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| A markdown node contains facts (e.g attributes) and the model used by markdown
|
|
||||||
-}
|
|
||||||
type alias MarkdownNodeRecord msg =
|
|
||||||
{ facts : Facts msg
|
|
||||||
, model : MarkdownModel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| Custom nodes contain facts (e.g attributes) and a json value for the model
|
|
||||||
-}
|
|
||||||
type alias CustomNodeRecord msg =
|
|
||||||
{ facts : Facts msg
|
|
||||||
, model : Json.Decode.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| Tagger holds the map function when Html.Map is used, the tagger
|
|
||||||
should then be applied to events comming from descendant nodes, it
|
|
||||||
is basically a javascript function.
|
|
||||||
-}
|
|
||||||
type alias Tagger =
|
|
||||||
Json.Decode.Value
|
|
||||||
|
|
||||||
|
|
||||||
{-| EventHandler holds the function that is called when an event is
|
|
||||||
triggered, it is basically a javascript object like this:
|
|
||||||
|
|
||||||
{ decoder: [Function] }
|
|
||||||
|
|
||||||
-}
|
|
||||||
type alias EventHandler =
|
|
||||||
Json.Decode.Value
|
|
||||||
|
|
||||||
|
|
||||||
{-| Facts contain various dictionaries and values for a node
|
|
||||||
|
|
||||||
- styles are a mapping of rules
|
|
||||||
- events may be a json object containing event handlers
|
|
||||||
- attributes are pulled out into stringAttributes and boolAttributes - things with string values go into
|
|
||||||
stringAttributes, things with bool values go into boolAttributes
|
|
||||||
|
|
||||||
-}
|
|
||||||
type alias Facts msg =
|
|
||||||
{ styles : Dict String String
|
|
||||||
, events : Dict String (Json.Decode.Decoder msg)
|
|
||||||
, attributeNamespace : Maybe Json.Decode.Value
|
|
||||||
, stringAttributes : Dict String String
|
|
||||||
, boolAttributes : Dict String Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| Type for representing the five kinds of elements according to HTML 5
|
|
||||||
[spec](https://html.spec.whatwg.org/multipage/syntax.html#elements-2).
|
|
||||||
Used to handle different rendering behavior depending on the type of element.
|
|
||||||
-}
|
|
||||||
type ElementKind
|
|
||||||
= VoidElements
|
|
||||||
| RawTextElements
|
|
||||||
| EscapableRawTextElements
|
|
||||||
| ForeignElements
|
|
||||||
| NormalElements
|
|
||||||
|
|
||||||
|
|
||||||
type HtmlContext msg
|
|
||||||
= HtmlContext (List Tagger) (List Tagger -> EventHandler -> Json.Decode.Decoder msg)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Type for representing Elm's Attributes
|
|
||||||
|
|
||||||
- Attribute is an HTML attribute, like `Html.Attributes.colspan`. These values
|
|
||||||
are applied using `element.setAttribute(key, value)` during a patch.
|
|
||||||
- NamespacedAttribute has an namespace, like `Svg.Attributes.xlinkHref`
|
|
||||||
- Property assigns a value to a node like `Html.Attributes.class`, and can
|
|
||||||
hold any encoded value. Unlike attributes, where `element.setAttribute()` is
|
|
||||||
used during the patch, properties are applied directly as
|
|
||||||
`element[key] = value`.
|
|
||||||
- Styles hold a list of key value pairs to be applied to the node's style set
|
|
||||||
- Event contains a decoder for a msg and the `Html.Event.Options` for the event
|
|
||||||
|
|
||||||
-}
|
|
||||||
type Attribute
|
|
||||||
= Attribute AttributeRecord
|
|
||||||
| NamespacedAttribute NamespacedAttributeRecord
|
|
||||||
| Property PropertyRecord
|
|
||||||
| Styles (List ( String, String ))
|
|
||||||
| Event EventRecord
|
|
||||||
|
|
||||||
|
|
||||||
{-| Attribute contains a string key and a string value
|
|
||||||
-}
|
|
||||||
type alias AttributeRecord =
|
|
||||||
{ key : String
|
|
||||||
, value : String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| NamespacedAttribute contains a string key, string namespace and string value
|
|
||||||
-}
|
|
||||||
type alias NamespacedAttributeRecord =
|
|
||||||
{ key : String
|
|
||||||
, value : String
|
|
||||||
, namespace : String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| Property contains a string key and a value with an arbitrary type
|
|
||||||
-}
|
|
||||||
type alias PropertyRecord =
|
|
||||||
{ key : String
|
|
||||||
, value : Json.Decode.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| Event contains a string key, a decoder for a msg and event options
|
|
||||||
-}
|
|
||||||
type alias EventRecord =
|
|
||||||
{ key : String
|
|
||||||
, decoder : Json.Decode.Value
|
|
||||||
, options : Html.Events.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode a json object into ElmHtml, you have to pass a function that decodes
|
|
||||||
events from Html Nodes. If you don't want to decode event msgs, you can ignore it:
|
|
||||||
|
|
||||||
decodeElmHtml (\_ _ -> ()) jsonHtml
|
|
||||||
|
|
||||||
if you do want to decode them, you will probably need to write some native code
|
|
||||||
like elm-html-test does to extract the function inside those.
|
|
||||||
|
|
||||||
-}
|
|
||||||
decodeElmHtml : (List Tagger -> EventHandler -> Json.Decode.Decoder msg) -> Json.Decode.Decoder (ElmHtml msg)
|
|
||||||
decodeElmHtml eventDecoder =
|
|
||||||
contextDecodeElmHtml (HtmlContext [] eventDecoder)
|
|
||||||
|
|
||||||
|
|
||||||
contextDecodeElmHtml : HtmlContext msg -> Json.Decode.Decoder (ElmHtml msg)
|
|
||||||
contextDecodeElmHtml context =
|
|
||||||
field "type" Json.Decode.string
|
|
||||||
|> Json.Decode.andThen
|
|
||||||
(\typeString ->
|
|
||||||
case typeString of
|
|
||||||
"text" ->
|
|
||||||
Json.Decode.map TextTag decodeTextTag
|
|
||||||
|
|
||||||
"keyed-node" ->
|
|
||||||
Json.Decode.map NodeEntry (decodeKeyedNode context)
|
|
||||||
|
|
||||||
"node" ->
|
|
||||||
Json.Decode.map NodeEntry (decodeNode context)
|
|
||||||
|
|
||||||
"custom" ->
|
|
||||||
decodeCustomNode context
|
|
||||||
|
|
||||||
"tagger" ->
|
|
||||||
decodeTagger context
|
|
||||||
|
|
||||||
"thunk" ->
|
|
||||||
field "node" (contextDecodeElmHtml context)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Json.Decode.fail ("No such type as " ++ typeString)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode text tag
|
|
||||||
-}
|
|
||||||
decodeTextTag : Json.Decode.Decoder TextTagRecord
|
|
||||||
decodeTextTag =
|
|
||||||
field "text" (Json.Decode.andThen (\text -> Json.Decode.succeed { text = text }) Json.Decode.string)
|
|
||||||
|
|
||||||
|
|
||||||
{-| encode text tag
|
|
||||||
-}
|
|
||||||
encodeTextTag : TextTagRecord -> Json.Encode.Value
|
|
||||||
encodeTextTag { text } =
|
|
||||||
Json.Encode.object [ ( "text", Json.Encode.string text ) ]
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode a tagger
|
|
||||||
-}
|
|
||||||
decodeTagger : HtmlContext msg -> Json.Decode.Decoder (ElmHtml msg)
|
|
||||||
decodeTagger (HtmlContext taggers eventDecoder) =
|
|
||||||
Json.Decode.field "tagger" Json.Decode.value
|
|
||||||
|> Json.Decode.andThen
|
|
||||||
(\tagger ->
|
|
||||||
let
|
|
||||||
nodeDecoder =
|
|
||||||
contextDecodeElmHtml (HtmlContext (taggers ++ [ tagger ]) eventDecoder)
|
|
||||||
in
|
|
||||||
Json.Decode.oneOf
|
|
||||||
[ Json.Decode.at [ "node" ] nodeDecoder
|
|
||||||
, Json.Decode.at [ "text" ] nodeDecoder
|
|
||||||
, Json.Decode.at [ "custom" ] nodeDecoder
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
decodeKeyedNode : HtmlContext msg -> Json.Decode.Decoder (NodeRecord msg)
|
|
||||||
decodeKeyedNode context =
|
|
||||||
let
|
|
||||||
-- elm stores keyed nodes as tuples
|
|
||||||
-- we only want to decode the html, in the second property
|
|
||||||
decodeSecondNode =
|
|
||||||
Json.Decode.field "_1" (contextDecodeElmHtml context)
|
|
||||||
in
|
|
||||||
Json.Decode.map4 NodeRecord
|
|
||||||
(Json.Decode.field "tag" Json.Decode.string)
|
|
||||||
(Json.Decode.field "children" (Json.Decode.list decodeSecondNode))
|
|
||||||
(Json.Decode.field "facts" (decodeFacts context))
|
|
||||||
(Json.Decode.field "descendantsCount" Json.Decode.int)
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode a node record
|
|
||||||
-}
|
|
||||||
decodeNode : HtmlContext msg -> Json.Decode.Decoder (NodeRecord msg)
|
|
||||||
decodeNode context =
|
|
||||||
Json.Decode.map4 NodeRecord
|
|
||||||
(field "tag" Json.Decode.string)
|
|
||||||
(field "children" (Json.Decode.list (contextDecodeElmHtml context)))
|
|
||||||
(field "facts" (decodeFacts context))
|
|
||||||
(field "descendantsCount" Json.Decode.int)
|
|
||||||
|
|
||||||
|
|
||||||
{-| encode a node record: currently does not support facts or children
|
|
||||||
-}
|
|
||||||
encodeNodeRecord : NodeRecord msg -> Json.Encode.Value
|
|
||||||
encodeNodeRecord record =
|
|
||||||
Json.Encode.object
|
|
||||||
[ ( "tag", Json.Encode.string record.tag )
|
|
||||||
|
|
||||||
--, ( "children", Json.Encode.list encodeElmHtml)
|
|
||||||
--, ( "facts", encodeFacts)
|
|
||||||
, ( "descendantsCount", Json.Encode.int record.descendantsCount )
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode custom node into either markdown or custom
|
|
||||||
-}
|
|
||||||
decodeCustomNode : HtmlContext msg -> Json.Decode.Decoder (ElmHtml msg)
|
|
||||||
decodeCustomNode context =
|
|
||||||
Json.Decode.oneOf
|
|
||||||
[ Json.Decode.map MarkdownNode (decodeMarkdownNodeRecord context)
|
|
||||||
, Json.Decode.map CustomNode (decodeCustomNodeRecord context)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode custom node record
|
|
||||||
-}
|
|
||||||
decodeCustomNodeRecord : HtmlContext msg -> Json.Decode.Decoder (CustomNodeRecord msg)
|
|
||||||
decodeCustomNodeRecord context =
|
|
||||||
Json.Decode.map2 CustomNodeRecord
|
|
||||||
(field "facts" (decodeFacts context))
|
|
||||||
(field "model" Json.Decode.value)
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode markdown node record
|
|
||||||
-}
|
|
||||||
decodeMarkdownNodeRecord : HtmlContext msg -> Json.Decode.Decoder (MarkdownNodeRecord msg)
|
|
||||||
decodeMarkdownNodeRecord context =
|
|
||||||
Json.Decode.map2 MarkdownNodeRecord
|
|
||||||
(field "facts" (decodeFacts context))
|
|
||||||
(field "model" decodeMarkdownModel)
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode the styles
|
|
||||||
-}
|
|
||||||
decodeStyles : Json.Decode.Decoder (Dict String String)
|
|
||||||
decodeStyles =
|
|
||||||
Json.Decode.oneOf
|
|
||||||
[ field styleKey (Json.Decode.dict Json.Decode.string)
|
|
||||||
, Json.Decode.succeed Dict.empty
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
{-| encode styles
|
|
||||||
-}
|
|
||||||
encodeStyles : Dict String String -> Json.Encode.Value
|
|
||||||
encodeStyles stylesDict =
|
|
||||||
let
|
|
||||||
encodedDict =
|
|
||||||
stylesDict
|
|
||||||
|> Dict.toList
|
|
||||||
|> List.map (\( k, v ) -> ( k, Json.Encode.string v ))
|
|
||||||
in
|
|
||||||
Json.Encode.object [ ( styleKey, Json.Encode.object encodedDict ) ]
|
|
||||||
|
|
||||||
|
|
||||||
{-| grab things from attributes via a decoder, then anything that isn't filtered on
|
|
||||||
the object
|
|
||||||
-}
|
|
||||||
decodeOthers : Json.Decode.Decoder a -> Json.Decode.Decoder (Dict String a)
|
|
||||||
decodeOthers otherDecoder =
|
|
||||||
decodeAttributes otherDecoder
|
|
||||||
|> Json.Decode.andThen
|
|
||||||
(\attributes ->
|
|
||||||
decodeDictFilterMap otherDecoder
|
|
||||||
|> Json.Decode.map (filterKnownKeys >> Dict.union attributes)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
{-| For a given decoder, keep the values from a dict that pass the decoder
|
|
||||||
-}
|
|
||||||
decodeDictFilterMap : Json.Decode.Decoder a -> Json.Decode.Decoder (Dict String a)
|
|
||||||
decodeDictFilterMap decoder =
|
|
||||||
Json.Decode.dict Json.Decode.value
|
|
||||||
|> Json.Decode.map
|
|
||||||
(Dict.toList
|
|
||||||
>> List.filterMap
|
|
||||||
(\( key, value ) ->
|
|
||||||
case Json.Decode.decodeValue decoder value of
|
|
||||||
Err _ ->
|
|
||||||
Nothing
|
|
||||||
|
|
||||||
Ok v ->
|
|
||||||
Just ( key, v )
|
|
||||||
)
|
|
||||||
>> Dict.fromList
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
decodeAttributes : Json.Decode.Decoder a -> Json.Decode.Decoder (Dict String a)
|
|
||||||
decodeAttributes decoder =
|
|
||||||
Json.Decode.oneOf
|
|
||||||
[ Json.Decode.field attributeKey (decodeDictFilterMap decoder)
|
|
||||||
, Json.Decode.succeed Dict.empty
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
decodeEvents : (EventHandler -> Json.Decode.Decoder msg) -> Json.Decode.Decoder (Dict String (Json.Decode.Decoder msg))
|
|
||||||
decodeEvents taggedEventDecoder =
|
|
||||||
Json.Decode.oneOf
|
|
||||||
[ Json.Decode.field eventKey (Json.Decode.dict (Json.Decode.map taggedEventDecoder Json.Decode.value))
|
|
||||||
, Json.Decode.succeed Dict.empty
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode fact
|
|
||||||
-}
|
|
||||||
decodeFacts : HtmlContext msg -> Json.Decode.Decoder (Facts msg)
|
|
||||||
decodeFacts (HtmlContext taggers eventDecoder) =
|
|
||||||
Json.Decode.map5 Facts
|
|
||||||
decodeStyles
|
|
||||||
(decodeEvents (eventDecoder taggers))
|
|
||||||
(Json.Decode.maybe (Json.Decode.field attributeNamespaceKey Json.Decode.value))
|
|
||||||
(decodeOthers Json.Decode.string)
|
|
||||||
(decodeOthers Json.Decode.bool)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Just empty facts
|
|
||||||
-}
|
|
||||||
emptyFacts : Facts msg
|
|
||||||
emptyFacts =
|
|
||||||
{ styles = Dict.empty
|
|
||||||
, events = Dict.empty
|
|
||||||
, attributeNamespace = Nothing
|
|
||||||
, stringAttributes = Dict.empty
|
|
||||||
, boolAttributes = Dict.empty
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| Decode a JSON object into an Attribute. You have to pass a function that
|
|
||||||
decodes events from event attributes. If you don't want to decode event msgs,
|
|
||||||
you can ignore it:
|
|
||||||
|
|
||||||
decodeAttribute (\_ -> ()) jsonHtml
|
|
||||||
|
|
||||||
If you do want to decode them, you will probably need to write some native code
|
|
||||||
like elm-html-test does to extract the function inside those.
|
|
||||||
|
|
||||||
-}
|
|
||||||
decodeAttribute : Json.Decode.Decoder Attribute
|
|
||||||
decodeAttribute =
|
|
||||||
Json.Decode.field "key" Json.Decode.string
|
|
||||||
|> Json.Decode.andThen
|
|
||||||
(\key ->
|
|
||||||
if key == attributeKey then
|
|
||||||
Json.Decode.map2 AttributeRecord
|
|
||||||
(Json.Decode.field "realKey" Json.Decode.string)
|
|
||||||
(Json.Decode.field "value" Json.Decode.string)
|
|
||||||
|> Json.Decode.map Attribute
|
|
||||||
else if key == attributeNamespaceKey then
|
|
||||||
Json.Decode.map3 NamespacedAttributeRecord
|
|
||||||
(Json.Decode.field "realKey" Json.Decode.string)
|
|
||||||
(Json.Decode.at [ "value", "value" ] Json.Decode.string)
|
|
||||||
(Json.Decode.at [ "value", "namespace" ] Json.Decode.string)
|
|
||||||
|> Json.Decode.map NamespacedAttribute
|
|
||||||
else if key == styleKey then
|
|
||||||
Json.Decode.map2 (,)
|
|
||||||
(Json.Decode.field "_0" Json.Decode.string)
|
|
||||||
(Json.Decode.field "_1" Json.Decode.string)
|
|
||||||
|> elmListDecoder
|
|
||||||
|> Json.Decode.field "value"
|
|
||||||
|> Json.Decode.map Styles
|
|
||||||
else if key == eventKey then
|
|
||||||
Json.Decode.map3 EventRecord
|
|
||||||
(Json.Decode.field "realKey" Json.Decode.string)
|
|
||||||
(Json.Decode.at [ "value", "decoder" ] Json.Decode.value)
|
|
||||||
(Json.Decode.at [ "value", "options" ] decodeOptions)
|
|
||||||
|> Json.Decode.map Event
|
|
||||||
else
|
|
||||||
Json.Decode.field "value" Json.Decode.value
|
|
||||||
|> Json.Decode.map (PropertyRecord key >> Property)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
decodeOptions : Json.Decode.Decoder Html.Events.Options
|
|
||||||
decodeOptions =
|
|
||||||
Json.Decode.map2 Html.Events.Options
|
|
||||||
(Json.Decode.field "stopPropagation" Json.Decode.bool)
|
|
||||||
(Json.Decode.field "preventDefault" Json.Decode.bool)
|
|
||||||
|
|
||||||
|
|
||||||
elmListDecoder : Json.Decode.Decoder a -> Json.Decode.Decoder (List a)
|
|
||||||
elmListDecoder itemDecoder =
|
|
||||||
elmListDecoderHelp itemDecoder []
|
|
||||||
|> Json.Decode.map List.reverse
|
|
||||||
|
|
||||||
|
|
||||||
elmListDecoderHelp : Json.Decode.Decoder a -> List a -> Json.Decode.Decoder (List a)
|
|
||||||
elmListDecoderHelp itemDecoder items =
|
|
||||||
Json.Decode.field "ctor" Json.Decode.string
|
|
||||||
|> Json.Decode.andThen
|
|
||||||
(\ctor ->
|
|
||||||
case ctor of
|
|
||||||
"[]" ->
|
|
||||||
Json.Decode.succeed items
|
|
||||||
|
|
||||||
"::" ->
|
|
||||||
Json.Decode.field "_0" itemDecoder
|
|
||||||
|> Json.Decode.andThen
|
|
||||||
(\value ->
|
|
||||||
Json.Decode.field "_1" (elmListDecoderHelp itemDecoder (value :: items))
|
|
||||||
)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Json.Decode.fail <| "Unrecognized constructor for an Elm List: " ++ ctor
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
{-| A list of Void elements as defined by the HTML5 specification. These
|
|
||||||
elements must not have closing tags and most not be written as self closing
|
|
||||||
either
|
|
||||||
-}
|
|
||||||
voidElements : List String
|
|
||||||
voidElements =
|
|
||||||
[ "area"
|
|
||||||
, "base"
|
|
||||||
, "br"
|
|
||||||
, "col"
|
|
||||||
, "embed"
|
|
||||||
, "hr"
|
|
||||||
, "img"
|
|
||||||
, "input"
|
|
||||||
, "link"
|
|
||||||
, "meta"
|
|
||||||
, "param"
|
|
||||||
, "source"
|
|
||||||
, "track"
|
|
||||||
, "wbr"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
{-| A list of all Raw Text Elements as defined by the HTML5 specification. They
|
|
||||||
can contain only text and have restrictions on which characters can appear
|
|
||||||
within its innerHTML
|
|
||||||
-}
|
|
||||||
rawTextElements : List String
|
|
||||||
rawTextElements =
|
|
||||||
[ "script", "style" ]
|
|
||||||
|
|
||||||
|
|
||||||
{-| A list of all Escapable Raw Text Elements as defined by the HTML5
|
|
||||||
specification. They can have text and character references, but the text must
|
|
||||||
not contain an ambiguous ampersand along with addional restrictions:
|
|
||||||
<https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions>
|
|
||||||
-}
|
|
||||||
escapableRawTextElements : List String
|
|
||||||
escapableRawTextElements =
|
|
||||||
[ "textarea", "title" ]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{- Foreign elements are elements from the MathML namespace and the
|
|
||||||
SVG namespace. TODO: detect these nodes and handle them correctly. Right
|
|
||||||
now they will just be treated as Normal elements.
|
|
||||||
-}
|
|
||||||
|
|
||||||
|
|
||||||
{-| Identify the kind of element. Helper to convert an tag name into a type for
|
|
||||||
pattern matching.
|
|
||||||
-}
|
|
||||||
toElementKind : String -> ElementKind
|
|
||||||
toElementKind element =
|
|
||||||
if List.member element voidElements then
|
|
||||||
VoidElements
|
|
||||||
else if List.member element rawTextElements then
|
|
||||||
RawTextElements
|
|
||||||
else if List.member element escapableRawTextElements then
|
|
||||||
EscapableRawTextElements
|
|
||||||
else
|
|
||||||
-- All other allowed HTML elements are normal elements
|
|
||||||
NormalElements
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
module ElmHtml.Markdown exposing (..)
|
|
||||||
|
|
||||||
{-| Markdown helpers
|
|
||||||
|
|
||||||
@docs MarkdownOptions, MarkdownModel, baseMarkdownModel
|
|
||||||
|
|
||||||
@docs encodeOptions, encodeMarkdownModel, decodeMarkdownModel
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Json.Encode
|
|
||||||
import Json.Decode exposing (field)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Just a default markdown model
|
|
||||||
-}
|
|
||||||
baseMarkdownModel : MarkdownModel
|
|
||||||
baseMarkdownModel =
|
|
||||||
{ options =
|
|
||||||
{ githubFlavored = Just { tables = False, breaks = False }
|
|
||||||
, defaultHighlighting = Nothing
|
|
||||||
, sanitize = False
|
|
||||||
, smartypants = False
|
|
||||||
}
|
|
||||||
, markdown = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| options markdown expects
|
|
||||||
-}
|
|
||||||
type alias MarkdownOptions =
|
|
||||||
{ githubFlavored : Maybe { tables : Bool, breaks : Bool }
|
|
||||||
, defaultHighlighting : Maybe String
|
|
||||||
, sanitize : Bool
|
|
||||||
, smartypants : Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| An internal markdown model. Options are the things you give markdown, markdown is the string
|
|
||||||
-}
|
|
||||||
type alias MarkdownModel =
|
|
||||||
{ options : MarkdownOptions
|
|
||||||
, markdown : String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| We don't really care about encoding options right now
|
|
||||||
TODO: we will if we want to represent things as we do for elm-html
|
|
||||||
-}
|
|
||||||
encodeOptions : MarkdownOptions -> Json.Decode.Value
|
|
||||||
encodeOptions options =
|
|
||||||
Json.Encode.null
|
|
||||||
|
|
||||||
|
|
||||||
{-| encode markdown model
|
|
||||||
-}
|
|
||||||
encodeMarkdownModel : MarkdownModel -> Json.Decode.Value
|
|
||||||
encodeMarkdownModel model =
|
|
||||||
Json.Encode.object
|
|
||||||
[ ( "options", encodeOptions model.options )
|
|
||||||
, ( "markdown", Json.Encode.string model.markdown )
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
{-| decode a markdown model
|
|
||||||
-}
|
|
||||||
decodeMarkdownModel : Json.Decode.Decoder MarkdownModel
|
|
||||||
decodeMarkdownModel =
|
|
||||||
field "markdown" Json.Decode.string
|
|
||||||
|> Json.Decode.map (MarkdownModel baseMarkdownModel.options)
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
module ElmHtml.ToElmString
|
|
||||||
exposing
|
|
||||||
( toElmString
|
|
||||||
, nodeRecordToString
|
|
||||||
, toElmStringWithOptions
|
|
||||||
, FormatOptions
|
|
||||||
, defaultFormatOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| Convert ElmHtml to string of Elm code.
|
|
||||||
|
|
||||||
@docs nodeRecordToString, toElmString, toElmStringWithOptions
|
|
||||||
|
|
||||||
@docs FormatOptions, defaultFormatOptions
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import String
|
|
||||||
import Dict exposing (Dict)
|
|
||||||
import ElmHtml.InternalTypes exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Formatting options to be used for converting to string
|
|
||||||
-}
|
|
||||||
type alias FormatOptions =
|
|
||||||
{ indent : Int
|
|
||||||
, newLines : Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| default formatting options
|
|
||||||
-}
|
|
||||||
defaultFormatOptions : FormatOptions
|
|
||||||
defaultFormatOptions =
|
|
||||||
{ indent = 0
|
|
||||||
, newLines = False
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
nodeToLines : FormatOptions -> ElmHtml msg -> List String
|
|
||||||
nodeToLines options nodeType =
|
|
||||||
case nodeType of
|
|
||||||
TextTag { text } ->
|
|
||||||
[ "Html.text \"" ++ text ++ "\"" ]
|
|
||||||
|
|
||||||
NodeEntry record ->
|
|
||||||
nodeRecordToString options record
|
|
||||||
|
|
||||||
CustomNode record ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
MarkdownNode record ->
|
|
||||||
[ record.model.markdown ]
|
|
||||||
|
|
||||||
NoOp ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a given html node to a string based on the type
|
|
||||||
-}
|
|
||||||
toElmString : ElmHtml msg -> String
|
|
||||||
toElmString =
|
|
||||||
toElmStringWithOptions defaultFormatOptions
|
|
||||||
|
|
||||||
|
|
||||||
{-| same as toElmString, but with options
|
|
||||||
-}
|
|
||||||
toElmStringWithOptions : FormatOptions -> ElmHtml msg -> String
|
|
||||||
toElmStringWithOptions options =
|
|
||||||
nodeToLines options
|
|
||||||
>> String.join
|
|
||||||
(if options.newLines then
|
|
||||||
"\n"
|
|
||||||
else
|
|
||||||
""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a node record to a string. This basically takes the tag name, then
|
|
||||||
pulls all the facts into tag declaration, then goes through the children and
|
|
||||||
nests them under this one
|
|
||||||
-}
|
|
||||||
nodeRecordToString : FormatOptions -> NodeRecord msg -> List String
|
|
||||||
nodeRecordToString options { tag, children, facts } =
|
|
||||||
let
|
|
||||||
openTag : List (Maybe String) -> String
|
|
||||||
openTag extras =
|
|
||||||
let
|
|
||||||
trimmedExtras =
|
|
||||||
List.filterMap (\x -> x) extras
|
|
||||||
|> List.map String.trim
|
|
||||||
|> List.filter ((/=) "")
|
|
||||||
|
|
||||||
filling =
|
|
||||||
case trimmedExtras of
|
|
||||||
[] ->
|
|
||||||
""
|
|
||||||
|
|
||||||
more ->
|
|
||||||
" " ++ (String.join " " more)
|
|
||||||
in
|
|
||||||
"Html." ++ tag ++ " [" ++ filling
|
|
||||||
|
|
||||||
childrenStrings =
|
|
||||||
List.map (nodeToLines options) children
|
|
||||||
|> List.concat
|
|
||||||
|> List.map ((++) (String.repeat options.indent " "))
|
|
||||||
|
|
||||||
styles =
|
|
||||||
case Dict.toList facts.styles of
|
|
||||||
[] ->
|
|
||||||
Nothing
|
|
||||||
|
|
||||||
styles ->
|
|
||||||
styles
|
|
||||||
|> List.map (\( key, value ) -> "(\"" ++ key ++ "\",\"" ++ value ++ "\")")
|
|
||||||
|> String.join ", "
|
|
||||||
|> (\styleString -> "Html.Attributes.style [" ++ styleString ++ "]")
|
|
||||||
|> Just
|
|
||||||
|
|
||||||
classes =
|
|
||||||
Dict.get "className" facts.stringAttributes
|
|
||||||
|> Maybe.map (\name -> "Html.Attributes.class [\"" ++ name ++ "\"]")
|
|
||||||
|
|
||||||
stringAttributes =
|
|
||||||
Dict.filter (\k v -> k /= "className") facts.stringAttributes
|
|
||||||
|> Dict.toList
|
|
||||||
|> List.map (\( k, v ) -> "Html.Attributes." ++ k ++ " \"" ++ v ++ "\"")
|
|
||||||
|> String.join ", "
|
|
||||||
|> Just
|
|
||||||
|
|
||||||
boolAttributes =
|
|
||||||
Dict.toList facts.boolAttributes
|
|
||||||
|> List.map (\( k, v ) -> "Html.Attributes.property \"" ++ k ++ "\" <| Json.Encode.bool " ++ toString v)
|
|
||||||
|> String.join " "
|
|
||||||
|> Just
|
|
||||||
in
|
|
||||||
[ openTag [ classes, styles, stringAttributes, boolAttributes ] ]
|
|
||||||
++ [ " ] "
|
|
||||||
, "[ "
|
|
||||||
, String.join "" childrenStrings
|
|
||||||
, "]"
|
|
||||||
]
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
module ElmHtml.ToHtml exposing (toHtml, factsToAttributes)
|
|
||||||
|
|
||||||
{-| This module is particularly useful for putting parsed Html into Elm.Html at runtime.
|
|
||||||
Estentially allowing the user to use tools like html-to-elm on their code.
|
|
||||||
|
|
||||||
@docs toHtml, factsToAttributes
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import String
|
|
||||||
import Dict exposing (Dict)
|
|
||||||
import ElmHtml.InternalTypes exposing (..)
|
|
||||||
import Html
|
|
||||||
import Html.Attributes
|
|
||||||
import Html.Events
|
|
||||||
import Json.Encode
|
|
||||||
import Json.Decode
|
|
||||||
|
|
||||||
|
|
||||||
{-| Turns ElmHtml into normal Elm Html
|
|
||||||
-}
|
|
||||||
toHtml : ElmHtml msg -> Html.Html msg
|
|
||||||
toHtml elmHtml =
|
|
||||||
case elmHtml of
|
|
||||||
TextTag text ->
|
|
||||||
Html.text text.text
|
|
||||||
|
|
||||||
NodeEntry { tag, children, facts } ->
|
|
||||||
Html.node tag [] (List.map toHtml children)
|
|
||||||
|
|
||||||
CustomNode record ->
|
|
||||||
let
|
|
||||||
_ =
|
|
||||||
Debug.log "Custom node is not supported" ""
|
|
||||||
in
|
|
||||||
Html.text ""
|
|
||||||
|
|
||||||
MarkdownNode record ->
|
|
||||||
let
|
|
||||||
_ =
|
|
||||||
Debug.log "Markdown node is not supported" ""
|
|
||||||
in
|
|
||||||
Html.text ""
|
|
||||||
|
|
||||||
NoOp ->
|
|
||||||
Html.text ""
|
|
||||||
|
|
||||||
|
|
||||||
stylesToAttribute : Dict String String -> Html.Attribute msg
|
|
||||||
stylesToAttribute =
|
|
||||||
Dict.toList
|
|
||||||
>> Html.Attributes.style
|
|
||||||
|
|
||||||
|
|
||||||
eventsToAttributes : Dict String (Json.Decode.Decoder msg) -> List (Html.Attribute msg)
|
|
||||||
eventsToAttributes =
|
|
||||||
Dict.toList
|
|
||||||
>> List.map (\( x, y ) -> Html.Events.on x y)
|
|
||||||
|
|
||||||
|
|
||||||
stringAttributesToAttributes : Dict String String -> List (Html.Attribute msg)
|
|
||||||
stringAttributesToAttributes =
|
|
||||||
Dict.toList
|
|
||||||
>> List.map (\( x, y ) -> Html.Attributes.attribute x y)
|
|
||||||
|
|
||||||
|
|
||||||
boolAttributesToAttributes : Dict String Bool -> List (Html.Attribute msg)
|
|
||||||
boolAttributesToAttributes =
|
|
||||||
Dict.toList
|
|
||||||
>> List.map (\( x, y ) -> Html.Attributes.property x (Json.Encode.bool y))
|
|
||||||
|
|
||||||
|
|
||||||
{-| Turns a fact record into a list of attributes
|
|
||||||
-}
|
|
||||||
factsToAttributes : Facts msg -> List (Html.Attribute msg)
|
|
||||||
factsToAttributes facts =
|
|
||||||
List.concat
|
|
||||||
[ [ stylesToAttribute facts.styles ]
|
|
||||||
, eventsToAttributes facts.events
|
|
||||||
, stringAttributesToAttributes facts.stringAttributes
|
|
||||||
, boolAttributesToAttributes facts.boolAttributes
|
|
||||||
]
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
module ElmHtml.ToString
|
|
||||||
exposing
|
|
||||||
( nodeToString
|
|
||||||
, nodeRecordToString
|
|
||||||
, nodeToStringWithOptions
|
|
||||||
, FormatOptions
|
|
||||||
, defaultFormatOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| Convert ElmHtml to string.
|
|
||||||
|
|
||||||
@docs nodeRecordToString, nodeToString, nodeToStringWithOptions
|
|
||||||
|
|
||||||
@docs FormatOptions, defaultFormatOptions
|
|
||||||
-}
|
|
||||||
|
|
||||||
import String
|
|
||||||
import Dict exposing (Dict)
|
|
||||||
import ElmHtml.InternalTypes exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Formatting options to be used for converting to string
|
|
||||||
-}
|
|
||||||
type alias FormatOptions =
|
|
||||||
{ indent : Int
|
|
||||||
, newLines : Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{-| default formatting options
|
|
||||||
-}
|
|
||||||
defaultFormatOptions : FormatOptions
|
|
||||||
defaultFormatOptions =
|
|
||||||
{ indent = 0
|
|
||||||
, newLines = False
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
nodeToLines : FormatOptions -> ElmHtml msg -> List String
|
|
||||||
nodeToLines options nodeType =
|
|
||||||
case nodeType of
|
|
||||||
TextTag { text } ->
|
|
||||||
[ text ]
|
|
||||||
|
|
||||||
NodeEntry record ->
|
|
||||||
nodeRecordToString options record
|
|
||||||
|
|
||||||
CustomNode record ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
MarkdownNode record ->
|
|
||||||
[ record.model.markdown ]
|
|
||||||
|
|
||||||
NoOp ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a given html node to a string based on the type
|
|
||||||
-}
|
|
||||||
nodeToString : ElmHtml msg -> String
|
|
||||||
nodeToString =
|
|
||||||
nodeToStringWithOptions defaultFormatOptions
|
|
||||||
|
|
||||||
|
|
||||||
{-| same as nodeToString, but with options
|
|
||||||
-}
|
|
||||||
nodeToStringWithOptions : FormatOptions -> ElmHtml msg -> String
|
|
||||||
nodeToStringWithOptions options =
|
|
||||||
nodeToLines options
|
|
||||||
>> String.join
|
|
||||||
(if options.newLines then
|
|
||||||
"\n"
|
|
||||||
else
|
|
||||||
""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a node record to a string. This basically takes the tag name, then
|
|
||||||
pulls all the facts into tag declaration, then goes through the children and
|
|
||||||
nests them under this one
|
|
||||||
-}
|
|
||||||
nodeRecordToString : FormatOptions -> NodeRecord msg -> List String
|
|
||||||
nodeRecordToString options { tag, children, facts } =
|
|
||||||
let
|
|
||||||
openTag : List (Maybe String) -> String
|
|
||||||
openTag extras =
|
|
||||||
let
|
|
||||||
trimmedExtras =
|
|
||||||
List.filterMap (\x -> x) extras
|
|
||||||
|> List.map String.trim
|
|
||||||
|> List.filter ((/=) "")
|
|
||||||
|
|
||||||
filling =
|
|
||||||
case trimmedExtras of
|
|
||||||
[] ->
|
|
||||||
""
|
|
||||||
|
|
||||||
more ->
|
|
||||||
" " ++ (String.join " " more)
|
|
||||||
in
|
|
||||||
"<" ++ tag ++ filling ++ ">"
|
|
||||||
|
|
||||||
closeTag =
|
|
||||||
"</" ++ tag ++ ">"
|
|
||||||
|
|
||||||
childrenStrings =
|
|
||||||
List.map (nodeToLines options) children
|
|
||||||
|> List.concat
|
|
||||||
|> List.map ((++) (String.repeat options.indent " "))
|
|
||||||
|
|
||||||
styles =
|
|
||||||
case Dict.toList facts.styles of
|
|
||||||
[] ->
|
|
||||||
Nothing
|
|
||||||
|
|
||||||
styles ->
|
|
||||||
styles
|
|
||||||
|> List.map (\( key, value ) -> key ++ ":" ++ value ++ ";")
|
|
||||||
|> String.join ""
|
|
||||||
|> (\styleString -> "style=\"" ++ styleString ++ "\"")
|
|
||||||
|> Just
|
|
||||||
|
|
||||||
classes =
|
|
||||||
Dict.get "className" facts.stringAttributes
|
|
||||||
|> Maybe.map (\name -> "class=\"" ++ name ++ "\"")
|
|
||||||
|
|
||||||
stringAttributes =
|
|
||||||
Dict.filter (\k v -> k /= "className") facts.stringAttributes
|
|
||||||
|> Dict.toList
|
|
||||||
|> List.map (\( k, v ) -> k ++ "=\"" ++ v ++ "\"")
|
|
||||||
|> String.join " "
|
|
||||||
|> Just
|
|
||||||
|
|
||||||
boolAttributes =
|
|
||||||
Dict.toList facts.boolAttributes
|
|
||||||
|> List.map (\( k, v ) -> k ++ "=" ++ (String.toLower <| toString v))
|
|
||||||
|> String.join " "
|
|
||||||
|> Just
|
|
||||||
in
|
|
||||||
case toElementKind tag of
|
|
||||||
{- Void elements only have a start tag; end tags must not be
|
|
||||||
specified for void elements.
|
|
||||||
-}
|
|
||||||
VoidElements ->
|
|
||||||
[ openTag [ classes, styles, stringAttributes, boolAttributes ] ]
|
|
||||||
|
|
||||||
{- TODO: implement restrictions for RawTextElements,
|
|
||||||
EscapableRawTextElements. Also handle ForeignElements correctly.
|
|
||||||
For now just punt and use the previous behavior for all other
|
|
||||||
element kinds.
|
|
||||||
-}
|
|
||||||
_ ->
|
|
||||||
[ openTag [ classes, styles, stringAttributes, boolAttributes ] ]
|
|
||||||
++ childrenStrings
|
|
||||||
++ [ closeTag ]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
var _eeue56$elm_html_in_elm$Native_HtmlAsJson = (function() {
|
|
||||||
return {
|
|
||||||
unsafeCoerce: function(a) {
|
|
||||||
return a;
|
|
||||||
},
|
|
||||||
eventDecoder: function (event) {
|
|
||||||
return event.decoder;
|
|
||||||
},
|
|
||||||
eventHandler: F2(function(eventName, html) {
|
|
||||||
return html.facts.EVENT[eventName];
|
|
||||||
}),
|
|
||||||
taggerFunction: function(tagger) {
|
|
||||||
return tagger;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
module Tests exposing (..)
|
|
||||||
|
|
||||||
import Dict
|
|
||||||
import ElmHtml.InternalTypes exposing (Attribute(..), ElmHtml(..), EventHandler, Facts, NodeRecord, Tagger, decodeAttribute, decodeElmHtml)
|
|
||||||
import ElmHtml.ToHtml
|
|
||||||
import ElmHtml.ToElmString exposing (toElmString)
|
|
||||||
import Expect
|
|
||||||
import Html exposing (Html, button, div, input, text)
|
|
||||||
import Html.Attributes exposing (class, colspan, disabled, style, value)
|
|
||||||
import Html.Events exposing (onCheck, onClick, onInput)
|
|
||||||
import Json.Decode exposing (decodeValue)
|
|
||||||
import Json.Encode
|
|
||||||
import Native.HtmlAsJson
|
|
||||||
import Svg.Attributes exposing (xlinkHref)
|
|
||||||
import Test exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
elmHtml : Test
|
|
||||||
elmHtml =
|
|
||||||
describe "ElmHtml parsing"
|
|
||||||
[ test "parsing a node" <|
|
|
||||||
\() ->
|
|
||||||
div [] []
|
|
||||||
|> fromHtml
|
|
||||||
|> Expect.equal (Ok (NodeEntry decodedNode))
|
|
||||||
, test "parsing a text" <|
|
|
||||||
\() ->
|
|
||||||
text "foo"
|
|
||||||
|> fromHtml
|
|
||||||
|> Expect.equal (Ok (TextTag { text = "foo" }))
|
|
||||||
, test "parsing attributes" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
facts =
|
|
||||||
{ decodedFacts
|
|
||||||
| stringAttributes = Dict.fromList [ ( "className", "foo" ), ( "value", "bar" ) ]
|
|
||||||
, boolAttributes = Dict.fromList [ ( "disabled", True ) ]
|
|
||||||
}
|
|
||||||
|
|
||||||
expected =
|
|
||||||
{ decodedNode | tag = "button", facts = facts }
|
|
||||||
in
|
|
||||||
button [ class "foo", value "bar", disabled True ] []
|
|
||||||
|> fromHtml
|
|
||||||
|> Expect.equal (Ok (NodeEntry expected))
|
|
||||||
, test "parsing children" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
expected =
|
|
||||||
{ decodedNode
|
|
||||||
| children = [ NodeEntry decodedNode, TextTag { text = "foo" } ]
|
|
||||||
, descendantsCount = 2
|
|
||||||
}
|
|
||||||
in
|
|
||||||
div []
|
|
||||||
[ div [] []
|
|
||||||
, text "foo"
|
|
||||||
]
|
|
||||||
|> fromHtml
|
|
||||||
|> Expect.equal (Ok (NodeEntry expected))
|
|
||||||
, describe "parsing events"
|
|
||||||
[ testParsingEvent "click" (onClick SomeMsg)
|
|
||||||
, testParsingEvent "input" (onInput InputMsg)
|
|
||||||
, testParsingEvent "change" (onCheck CheckMsg)
|
|
||||||
]
|
|
||||||
, describe "parsing Html.map"
|
|
||||||
[ test "adds the correct tagger to a mapped button" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
taggedNode =
|
|
||||||
input [ onInput identity ] []
|
|
||||||
|> Html.map (\msg -> msg ++ "bar")
|
|
||||||
|> fromHtml
|
|
||||||
in
|
|
||||||
taggedNode
|
|
||||||
|> Result.andThen (simulate "input" "{\"target\": {\"value\": \"foo\"}}")
|
|
||||||
|> Expect.equal (Ok "foobar")
|
|
||||||
, test "adds two taggers to a double mapped button with changing types" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
taggedNode =
|
|
||||||
input [ onInput identity ] []
|
|
||||||
|> Html.map (\str -> [ str ] ++ [ "bar" ])
|
|
||||||
|> Html.map (\list -> ( list, "baz" ))
|
|
||||||
|> fromHtml
|
|
||||||
in
|
|
||||||
taggedNode
|
|
||||||
|> Result.andThen (simulate "input" "{\"target\": {\"value\": \"foo\"}}")
|
|
||||||
|> Expect.equal (Ok ( [ "foo", "bar" ], "baz" ))
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
elmHtmlToHtml : Test
|
|
||||||
elmHtmlToHtml =
|
|
||||||
describe "Turning the AST into Html"
|
|
||||||
[ test "parsing a node" <|
|
|
||||||
\() ->
|
|
||||||
div [] []
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map ElmHtml.ToHtml.toHtml
|
|
||||||
|> Expect.equal (Ok <| div [] [])
|
|
||||||
, test "parsing a text" <|
|
|
||||||
\() ->
|
|
||||||
text "foo"
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map ElmHtml.ToHtml.toHtml
|
|
||||||
|> Expect.equal (Ok <| text "foo")
|
|
||||||
, test "parsing a text in a div" <|
|
|
||||||
\() ->
|
|
||||||
div [] [ text "foo" ]
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map ElmHtml.ToHtml.toHtml
|
|
||||||
|> Expect.equal (Ok <| div [] [ text "foo" ])
|
|
||||||
, test "parsing a text in a div in a div in a div " <|
|
|
||||||
\() ->
|
|
||||||
div [] [ div [] [ text "banana", div [] [ text "foo", text "bar" ] ] ]
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map ElmHtml.ToHtml.toHtml
|
|
||||||
|> Expect.equal (Ok <| div [] [ div [] [ text "banana", div [] [ text "foo", text "bar" ] ] ])
|
|
||||||
, test "parsing styles in a div" <|
|
|
||||||
\() ->
|
|
||||||
div [ Html.Attributes.style [ ( "background", "red" ) ] ] [ text "foo" ]
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map ElmHtml.ToHtml.toHtml
|
|
||||||
|> Expect.equal (Ok <| div [ Html.Attributes.style [ ( "background", "red" ) ] ] [ text "foo" ])
|
|
||||||
, test "parsing attributes a div" <|
|
|
||||||
\() ->
|
|
||||||
div [ Html.Attributes.name "fish", Html.Attributes.checked True ] [ text "foo" ]
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map ElmHtml.ToHtml.toHtml
|
|
||||||
|> Expect.equal (Ok <| div [ Html.Attributes.name "fish", Html.Attributes.checked True ] [ text "foo" ])
|
|
||||||
, test "parsing attributes in a nested div" <|
|
|
||||||
\() ->
|
|
||||||
div [ Html.Attributes.name "fish", Html.Attributes.checked True ] [ Html.li [ Html.Attributes.type_ "hello" ] [ text "foo" ] ]
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map ElmHtml.ToHtml.toHtml
|
|
||||||
|> Expect.equal (Ok <| div [ Html.Attributes.name "fish", Html.Attributes.checked True ] [ Html.li [ Html.Attributes.type_ "hello" ] [ text "foo" ] ])
|
|
||||||
, test "parsing events in a div" <|
|
|
||||||
\() ->
|
|
||||||
div [ Html.Events.onClick True ] []
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map ElmHtml.ToHtml.toHtml
|
|
||||||
|> Expect.equal (Ok <| div [ Html.Events.onClick True ] [])
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
elmHtmlToElmString : Test
|
|
||||||
elmHtmlToElmString =
|
|
||||||
describe "Turning the AST into Elm, but as a string"
|
|
||||||
[ test "parsing a node" <|
|
|
||||||
\() ->
|
|
||||||
div [] []
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map toElmString
|
|
||||||
|> Expect.equal (Ok <| "Html.div [ ] [ ]")
|
|
||||||
, test "parsing a text" <|
|
|
||||||
\() ->
|
|
||||||
text "foo"
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map toElmString
|
|
||||||
|> Expect.equal (Ok <| "Html.text \"foo\"")
|
|
||||||
, test "parsing a nested node" <|
|
|
||||||
\() ->
|
|
||||||
div [] [ div [] [ text "hello" ] ]
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map toElmString
|
|
||||||
|> Expect.equal (Ok <| "Html.div [ ] [ Html.div [ ] [ Html.text \"hello\"]]")
|
|
||||||
, test "parsing an attribute" <|
|
|
||||||
\() ->
|
|
||||||
div [ Html.Attributes.checked True ] [ text "hello" ]
|
|
||||||
|> fromHtml
|
|
||||||
|> Result.map toElmString
|
|
||||||
|> Expect.equal (Ok <| "Html.div [ Html.Attributes.property \"checked\" <| Json.Encode.bool True ] [ Html.text \"hello\"]")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
attributes : Test
|
|
||||||
attributes =
|
|
||||||
describe "Attribute parsing"
|
|
||||||
[ test "parsing Attribute" <|
|
|
||||||
\() ->
|
|
||||||
colspan 1
|
|
||||||
|> fromAttribute
|
|
||||||
|> Expect.equal (Ok (Attribute { key = "colspan", value = "1" }))
|
|
||||||
, test "parsing NamespacedAttribute" <|
|
|
||||||
\() ->
|
|
||||||
xlinkHref "#id"
|
|
||||||
|> fromAttribute
|
|
||||||
|> Expect.equal
|
|
||||||
(Ok (NamespacedAttribute { key = "xlink:href", value = "#id", namespace = "http://www.w3.org/1999/xlink" }))
|
|
||||||
, test "parsing Property" <|
|
|
||||||
\() ->
|
|
||||||
disabled True
|
|
||||||
|> fromAttribute
|
|
||||||
|> Expect.equal (Ok (Property { key = "disabled", value = Json.Encode.bool True }))
|
|
||||||
, test "parsing Event" <|
|
|
||||||
\() ->
|
|
||||||
onClick ()
|
|
||||||
|> fromAttribute
|
|
||||||
|> Expect.equal
|
|
||||||
(Ok (Event { key = "click", decoder = toJson (Json.Decode.succeed ()), options = Html.Events.defaultOptions }))
|
|
||||||
, test "parsing Styles" <|
|
|
||||||
\() ->
|
|
||||||
style [ ( "margin", "0" ) ]
|
|
||||||
|> fromAttribute
|
|
||||||
|> Expect.equal (Ok (Styles [ ( "margin", "0" ) ]))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= SomeMsg
|
|
||||||
| InputMsg String
|
|
||||||
| CheckMsg Bool
|
|
||||||
|
|
||||||
|
|
||||||
toJson : a -> Json.Decode.Value
|
|
||||||
toJson =
|
|
||||||
Native.HtmlAsJson.unsafeCoerce
|
|
||||||
|
|
||||||
|
|
||||||
eventDecoder : EventHandler -> Json.Decode.Decoder msg
|
|
||||||
eventDecoder eventHandler =
|
|
||||||
Native.HtmlAsJson.eventDecoder eventHandler
|
|
||||||
|
|
||||||
|
|
||||||
eventHandler : String -> Html a -> Json.Decode.Value
|
|
||||||
eventHandler eventName node =
|
|
||||||
Native.HtmlAsJson.eventHandler eventName node
|
|
||||||
|
|
||||||
|
|
||||||
taggerFunction : Tagger -> (a -> msg)
|
|
||||||
taggerFunction tagger =
|
|
||||||
Native.HtmlAsJson.taggerFunction tagger
|
|
||||||
|
|
||||||
|
|
||||||
taggedEventDecoder : List Tagger -> EventHandler -> Json.Decode.Decoder msg
|
|
||||||
taggedEventDecoder taggers eventHandler =
|
|
||||||
case taggers of
|
|
||||||
[] ->
|
|
||||||
eventDecoder eventHandler
|
|
||||||
|
|
||||||
[ tagger ] ->
|
|
||||||
Json.Decode.map (taggerFunction tagger) (eventDecoder eventHandler)
|
|
||||||
|
|
||||||
tagger :: taggers ->
|
|
||||||
Json.Decode.map (taggerFunction tagger) (taggedEventDecoder taggers eventHandler)
|
|
||||||
|
|
||||||
|
|
||||||
fromAttribute : Html.Attribute a -> Result String Attribute
|
|
||||||
fromAttribute attribute =
|
|
||||||
toJson attribute
|
|
||||||
|> decodeValue decodeAttribute
|
|
||||||
|
|
||||||
|
|
||||||
decodedNode : NodeRecord msg
|
|
||||||
decodedNode =
|
|
||||||
{ tag = "div"
|
|
||||||
, children = []
|
|
||||||
, facts = decodedFacts
|
|
||||||
, descendantsCount = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
decodedFacts : Facts msg
|
|
||||||
decodedFacts =
|
|
||||||
{ styles = Dict.fromList []
|
|
||||||
, events = Dict.fromList []
|
|
||||||
, attributeNamespace = Nothing
|
|
||||||
, stringAttributes = Dict.fromList []
|
|
||||||
, boolAttributes = Dict.fromList []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fromHtml : Html a -> Result String (ElmHtml msg)
|
|
||||||
fromHtml html =
|
|
||||||
toJson html
|
|
||||||
|> decodeValue (decodeElmHtml taggedEventDecoder)
|
|
||||||
|
|
||||||
|
|
||||||
simulate : String -> String -> ElmHtml msg -> Result String msg
|
|
||||||
simulate eventName event parsedHtml =
|
|
||||||
case parsedHtml of
|
|
||||||
NodeEntry node ->
|
|
||||||
Dict.get eventName node.facts.events
|
|
||||||
|> Result.fromMaybe "Tried to trigger event on something other than a NodeEntry"
|
|
||||||
|> Result.andThen (\eventDecoder -> Json.Decode.decodeString eventDecoder event)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Err "Tried to trigger event on something other than a NodeEntry"
|
|
||||||
|
|
||||||
|
|
||||||
testParsingEvent : String -> Html.Attribute a -> Test
|
|
||||||
testParsingEvent eventName eventAttribute =
|
|
||||||
test ("parsing " ++ eventName) <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
node =
|
|
||||||
button [ eventAttribute ] []
|
|
||||||
|
|
||||||
facts =
|
|
||||||
{ decodedFacts
|
|
||||||
| events = Dict.fromList [ ( eventName, eventDecoder (eventHandler eventName node) ) ]
|
|
||||||
}
|
|
||||||
|
|
||||||
expected =
|
|
||||||
{ decodedNode | tag = "button", facts = facts }
|
|
||||||
in
|
|
||||||
node
|
|
||||||
|> fromHtml
|
|
||||||
|> Expect.equal (Ok (NodeEntry expected))
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "Test Suites",
|
|
||||||
"repository": "https://github.com/eeue56/elm-html-in-elm.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"../src",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"native-modules": true,
|
|
||||||
"dependencies": {
|
|
||||||
"elm-community/elm-test": "4.0.0 <= v < 5.0.0",
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
|
||||||
"elm-lang/svg": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# elm-package generated files
|
|
||||||
elm-stuff/
|
|
||||||
# elm-repl generated files
|
|
||||||
repl-temp-*
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2016, Noah
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# elm-html-query
|
|
||||||
|
|
||||||
Query things using a [ElmHtml](http://package.elm-lang.org/packages/eeue56/elm-html-in-elm/latest) representation. This project is used alongside [elm-html-test](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest) to implement Html-based tests in Elm.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "3.0.0",
|
|
||||||
"summary": "Query for things inside ElmHtml",
|
|
||||||
"repository": "https://github.com/eeue56/elm-html-query.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [
|
|
||||||
"ElmHtml.Query"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"eeue56/elm-html-in-elm": "5.0.0 <= v < 6.0.0",
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
module ElmHtml.Query
|
|
||||||
exposing
|
|
||||||
( Selector(..)
|
|
||||||
, query
|
|
||||||
, queryAll
|
|
||||||
, queryInNode
|
|
||||||
, queryChildren
|
|
||||||
, queryChildrenAll
|
|
||||||
, queryById
|
|
||||||
, queryByClassName
|
|
||||||
, queryByClassList
|
|
||||||
, queryByStyle
|
|
||||||
, queryByTagName
|
|
||||||
, queryByAttribute
|
|
||||||
, queryByBoolAttribute
|
|
||||||
, getChildren
|
|
||||||
)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Query things using ElmHtml
|
|
||||||
|
|
||||||
@docs Selector
|
|
||||||
@docs query, queryAll, queryChildren, queryChildrenAll, queryInNode
|
|
||||||
@docs queryById, queryByClassName, queryByClassList, queryByStyle, queryByTagName, queryByAttribute, queryByBoolAttribute
|
|
||||||
@docs getChildren
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Dict
|
|
||||||
import String
|
|
||||||
import ElmHtml.InternalTypes exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Selectors to query a Html element
|
|
||||||
- Id, classname, classlist, tag are all what you'd expect
|
|
||||||
- Attribute and bool attribute are attributes
|
|
||||||
- ConainsText just searches inside for the given text
|
|
||||||
-}
|
|
||||||
type Selector
|
|
||||||
= Id String
|
|
||||||
| ClassName String
|
|
||||||
| ClassList (List String)
|
|
||||||
| Tag String
|
|
||||||
| Attribute String String
|
|
||||||
| BoolAttribute String Bool
|
|
||||||
| Style (List ( String, String ))
|
|
||||||
| ContainsText String
|
|
||||||
| Multiple (List Selector)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query for a node with a given tag in a Html element
|
|
||||||
-}
|
|
||||||
queryByTagName : String -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryByTagName tagname =
|
|
||||||
query (Tag tagname)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query for a node with a given id in a Html element
|
|
||||||
-}
|
|
||||||
queryById : String -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryById id =
|
|
||||||
query (Id id)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query for a node with a given classname in a Html element
|
|
||||||
-}
|
|
||||||
queryByClassName : String -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryByClassName classname =
|
|
||||||
query (ClassName classname)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query for a node with all the given classnames in a Html element
|
|
||||||
-}
|
|
||||||
queryByClassList : List String -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryByClassList classList =
|
|
||||||
query (ClassList classList)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query for a node with the given style in a Html element
|
|
||||||
-}
|
|
||||||
queryByStyle : List ( String, String ) -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryByStyle style =
|
|
||||||
query (Style style)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query for a node with a given attribute in a Html element
|
|
||||||
-}
|
|
||||||
queryByAttribute : String -> String -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryByAttribute key value =
|
|
||||||
query (Attribute key value)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query for a node with a given attribute in a Html element
|
|
||||||
-}
|
|
||||||
queryByBoolAttribute : String -> Bool -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryByBoolAttribute key value =
|
|
||||||
query (BoolAttribute key value)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query an ElmHtml element using a selector, searching all children.
|
|
||||||
-}
|
|
||||||
query : Selector -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
query selector =
|
|
||||||
queryInNode selector
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query an ElmHtml node using multiple selectors, considering both the node itself
|
|
||||||
as well as all of its descendants.
|
|
||||||
-}
|
|
||||||
queryAll : List Selector -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryAll selectors =
|
|
||||||
query (Multiple selectors)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query an ElmHtml node using a selector, considering both the node itself
|
|
||||||
as well as all of its descendants.
|
|
||||||
-}
|
|
||||||
queryInNode : Selector -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryInNode =
|
|
||||||
queryInNodeHelp Nothing
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query an ElmHtml node using a selector, considering both the node itself
|
|
||||||
as well as all of its descendants.
|
|
||||||
-}
|
|
||||||
queryChildren : Selector -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryChildren =
|
|
||||||
queryInNodeHelp (Just 1)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Returns just the immediate children of an ElmHtml node
|
|
||||||
-}
|
|
||||||
getChildren : ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
getChildren elmHtml =
|
|
||||||
case elmHtml of
|
|
||||||
NodeEntry { children } ->
|
|
||||||
children
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
|
|
||||||
{-| Query to ensure an ElmHtml node has all selectors given, without considering
|
|
||||||
any descendants lower than its immediate children.
|
|
||||||
-}
|
|
||||||
queryChildrenAll : List Selector -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryChildrenAll selectors =
|
|
||||||
queryInNodeHelp (Just 1) (Multiple selectors)
|
|
||||||
|
|
||||||
|
|
||||||
queryInNodeHelp : Maybe Int -> Selector -> ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
queryInNodeHelp maxDescendantDepth selector node =
|
|
||||||
case node of
|
|
||||||
NodeEntry record ->
|
|
||||||
let
|
|
||||||
childEntries =
|
|
||||||
descendInQuery maxDescendantDepth selector record.children
|
|
||||||
in
|
|
||||||
if predicateFromSelector selector node then
|
|
||||||
node :: childEntries
|
|
||||||
else
|
|
||||||
childEntries
|
|
||||||
|
|
||||||
TextTag { text } ->
|
|
||||||
case selector of
|
|
||||||
ContainsText innerText ->
|
|
||||||
if String.contains innerText text then
|
|
||||||
[ node ]
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
MarkdownNode { facts, model } ->
|
|
||||||
if predicateFromSelector selector node then
|
|
||||||
[ node ]
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
|
|
||||||
descendInQuery : Maybe Int -> Selector -> List (ElmHtml msg) -> List (ElmHtml msg)
|
|
||||||
descendInQuery maxDescendantDepth selector children =
|
|
||||||
case maxDescendantDepth of
|
|
||||||
Nothing ->
|
|
||||||
-- No maximum, so continue.
|
|
||||||
List.concatMap
|
|
||||||
(queryInNodeHelp Nothing selector)
|
|
||||||
children
|
|
||||||
|
|
||||||
Just depth ->
|
|
||||||
if depth > 0 then
|
|
||||||
-- Continue with maximum depth reduced by 1.
|
|
||||||
List.concatMap
|
|
||||||
(queryInNodeHelp (Just (depth - 1)) selector)
|
|
||||||
children
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
|
|
||||||
|
|
||||||
predicateFromSelector : Selector -> ElmHtml msg -> Bool
|
|
||||||
predicateFromSelector selector html =
|
|
||||||
case html of
|
|
||||||
NodeEntry record ->
|
|
||||||
record
|
|
||||||
|> nodeRecordPredicate selector
|
|
||||||
|
|
||||||
MarkdownNode markdownModel ->
|
|
||||||
markdownModel
|
|
||||||
|> markdownPredicate selector
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
hasAllSelectors : List Selector -> ElmHtml msg -> Bool
|
|
||||||
hasAllSelectors selectors record =
|
|
||||||
List.map predicateFromSelector selectors
|
|
||||||
|> List.map (\selector -> selector record)
|
|
||||||
|> List.all identity
|
|
||||||
|
|
||||||
|
|
||||||
hasAttribute : String -> String -> Facts msg -> Bool
|
|
||||||
hasAttribute attribute query facts =
|
|
||||||
case Dict.get attribute facts.stringAttributes of
|
|
||||||
Just id ->
|
|
||||||
id == query
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
hasBoolAttribute : String -> Bool -> Facts msg -> Bool
|
|
||||||
hasBoolAttribute attribute value facts =
|
|
||||||
case Dict.get attribute facts.boolAttributes of
|
|
||||||
Just id ->
|
|
||||||
id == value
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
hasClass : String -> Facts msg -> Bool
|
|
||||||
hasClass query facts =
|
|
||||||
List.member query (classnames facts)
|
|
||||||
|
|
||||||
|
|
||||||
hasClasses : List String -> Facts msg -> Bool
|
|
||||||
hasClasses classList facts =
|
|
||||||
containsAll classList (classnames facts)
|
|
||||||
|
|
||||||
|
|
||||||
hasStyle : List ( String, String ) -> Facts msg -> Bool
|
|
||||||
hasStyle style facts =
|
|
||||||
containsAll style (Dict.toList facts.styles)
|
|
||||||
|
|
||||||
|
|
||||||
classnames : Facts msg -> List String
|
|
||||||
classnames facts =
|
|
||||||
Dict.get "className" facts.stringAttributes
|
|
||||||
|> Maybe.withDefault ""
|
|
||||||
|> String.split " "
|
|
||||||
|
|
||||||
|
|
||||||
containsAll : List a -> List a -> Bool
|
|
||||||
containsAll a b =
|
|
||||||
b
|
|
||||||
|> List.foldl (\i acc -> List.filter ((/=) i) acc) a
|
|
||||||
|> List.isEmpty
|
|
||||||
|
|
||||||
|
|
||||||
nodeRecordPredicate : Selector -> (NodeRecord msg -> Bool)
|
|
||||||
nodeRecordPredicate selector =
|
|
||||||
case selector of
|
|
||||||
Id id ->
|
|
||||||
.facts
|
|
||||||
>> hasAttribute "id" id
|
|
||||||
|
|
||||||
ClassName classname ->
|
|
||||||
.facts
|
|
||||||
>> hasClass classname
|
|
||||||
|
|
||||||
ClassList classList ->
|
|
||||||
.facts
|
|
||||||
>> hasClasses classList
|
|
||||||
|
|
||||||
Tag tag ->
|
|
||||||
.tag
|
|
||||||
>> (==) tag
|
|
||||||
|
|
||||||
Attribute key value ->
|
|
||||||
.facts
|
|
||||||
>> hasAttribute key value
|
|
||||||
|
|
||||||
BoolAttribute key value ->
|
|
||||||
.facts
|
|
||||||
>> hasBoolAttribute key value
|
|
||||||
|
|
||||||
Style style ->
|
|
||||||
.facts
|
|
||||||
>> hasStyle style
|
|
||||||
|
|
||||||
ContainsText text ->
|
|
||||||
always False
|
|
||||||
|
|
||||||
Multiple selectors ->
|
|
||||||
NodeEntry
|
|
||||||
>> hasAllSelectors selectors
|
|
||||||
|
|
||||||
|
|
||||||
markdownPredicate : Selector -> (MarkdownNodeRecord msg -> Bool)
|
|
||||||
markdownPredicate selector =
|
|
||||||
case selector of
|
|
||||||
Id id ->
|
|
||||||
.facts
|
|
||||||
>> hasAttribute "id" id
|
|
||||||
|
|
||||||
ClassName classname ->
|
|
||||||
.facts
|
|
||||||
>> hasClass classname
|
|
||||||
|
|
||||||
ClassList classList ->
|
|
||||||
.facts
|
|
||||||
>> hasClasses classList
|
|
||||||
|
|
||||||
Tag tag ->
|
|
||||||
always False
|
|
||||||
|
|
||||||
Attribute key value ->
|
|
||||||
.facts
|
|
||||||
>> hasAttribute key value
|
|
||||||
|
|
||||||
BoolAttribute key value ->
|
|
||||||
.facts
|
|
||||||
>> hasBoolAttribute key value
|
|
||||||
|
|
||||||
Style style ->
|
|
||||||
.facts
|
|
||||||
>> hasStyle style
|
|
||||||
|
|
||||||
ContainsText text ->
|
|
||||||
.model
|
|
||||||
>> .markdown
|
|
||||||
>> String.contains text
|
|
||||||
|
|
||||||
Multiple selectors ->
|
|
||||||
MarkdownNode
|
|
||||||
>> hasAllSelectors selectors
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# elm-package generated files
|
|
||||||
elm-stuff/
|
|
||||||
# elm-repl generated files
|
|
||||||
repl-temp-*
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- tests/elm-stuff/build-artifacts
|
|
||||||
|
|
||||||
os:
|
|
||||||
- osx
|
|
||||||
- linux
|
|
||||||
|
|
||||||
env:
|
|
||||||
matrix:
|
|
||||||
- ELM_VERSION=0.18 TARGET_NODE_VERSION=node
|
|
||||||
- ELM_VERSION=0.18 TARGET_NODE_VERSION=4.2
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- if [ ${TRAVIS_OS_NAME} == "osx" ];
|
|
||||||
then brew update; brew install nvm; mkdir ~/.nvm; export NVM_DIR=~/.nvm; source $(brew --prefix nvm)/nvm.sh;
|
|
||||||
fi
|
|
||||||
|
|
||||||
install:
|
|
||||||
- nvm install $TARGET_NODE_VERSION
|
|
||||||
- nvm use $TARGET_NODE_VERSION
|
|
||||||
- node --version
|
|
||||||
- npm --version
|
|
||||||
- npm install -g elm@$ELM_VERSION
|
|
||||||
- npm install -g elm-test@beta
|
|
||||||
|
|
||||||
|
|
||||||
script:
|
|
||||||
- elm-test
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2016, Noah Hall, Richard Feldman
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# elm-html-test
|
|
||||||
|
|
||||||
Test views by writing expectations about `Html` values. [](https://travis-ci.org/eeue56/elm-html-test)
|
|
||||||
|
|
||||||
```elm
|
|
||||||
import Html
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test.Html.Selector exposing (text, tag)
|
|
||||||
|
|
||||||
|
|
||||||
test "Button has the expected text" <|
|
|
||||||
\() ->
|
|
||||||
Html.div [ class "container" ]
|
|
||||||
[ Html.button [] [ Html.text "I'm a button!" ] ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find [ tag "button" ]
|
|
||||||
|> Query.has [ text "I'm a button!" ]
|
|
||||||
```
|
|
||||||
|
|
||||||
These tests are designed to be written in a pipeline like this:
|
|
||||||
|
|
||||||
1. Call [`Query.fromHtml`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#fromHtml) on your [`Html`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html#Html) to begin querying it.
|
|
||||||
2. Use queries like [`Query.find`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#find), [`Query.findAll`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#findAll), and [`Query.children`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#children) to find the elements to test.
|
|
||||||
3. Create expectations using things like [`Query.has`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#has) and [`Query.count`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#count).
|
|
||||||
|
|
||||||
These are normal expectations, so you can use them with [`fuzz`](http://package.elm-lang.org/packages/elm-community/elm-test/latest/Test#fuzz) just as easily as with [`test`](http://package.elm-lang.org/packages/elm-community/elm-test/3.1.0/Test#test)!
|
|
||||||
|
|
||||||
## Querying
|
|
||||||
|
|
||||||
Queries come in two flavors: [`Single`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#Single) and [`Multiple`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#Multiple).
|
|
||||||
|
|
||||||
This is because some queries are expected to return a single result, whereas
|
|
||||||
others may return multiple results.
|
|
||||||
|
|
||||||
If a `Single` query finds exactly one result, it will succeed and continue with
|
|
||||||
any further querying or expectations. If it finds zero results, or more than one,
|
|
||||||
the test will fail.
|
|
||||||
|
|
||||||
Since other querying and expectation functions are written in terms of `Single`
|
|
||||||
and `Multiple`, the compiler can help make sure queries are connected as
|
|
||||||
expected. For example, [`count`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#count) accepts a `Multiple`, because counting a single element does not make much sense!
|
|
||||||
|
|
||||||
If you have a `Multiple` and want to use an expectation that works on a `Single`,
|
|
||||||
such as [`Query.has`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#has), you can use [`Query.each`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Query#each) to run the expectation on each of the elements in the `Multiple`.
|
|
||||||
|
|
||||||
## Selecting elements by `Html.Attribute msg`
|
|
||||||
|
|
||||||
Ordinary `Html.Attribute msg` values can be used to select elements using
|
|
||||||
`Test.Html.Selector.attribute`. It is important when using this selector to
|
|
||||||
understand its behavior.
|
|
||||||
|
|
||||||
- `Html.Attributes.class` and `Html.Attributes.classList` will work the same as
|
|
||||||
[`Test.Html.Selector.classes`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Selector#classes),
|
|
||||||
matching any element with at least the given classes. This behavior echoes
|
|
||||||
that of `element.querySelectorAll('.my-class')` from JavaScript, where any
|
|
||||||
element with at least `.my-class` will match the query.
|
|
||||||
|
|
||||||
- `Html.Attributes.style` will work the same way as
|
|
||||||
[`Test.Html.Selector.styles`](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest/Test-Html-Selector#styles),
|
|
||||||
matching any element with at least the given style properties.
|
|
||||||
|
|
||||||
- Any other `String` attributes and properties like `title`, or `Bool`
|
|
||||||
attributes like `disabled` will match elements with the exact value for those
|
|
||||||
attributes.
|
|
||||||
|
|
||||||
- Any attributes from `Html.Events`, or attributes with values that have types
|
|
||||||
other than `String` or `Bool` will not match anything.
|
|
||||||
|
|
||||||
The example below demonstrates usage
|
|
||||||
|
|
||||||
```elm
|
|
||||||
import Html
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Test exposing (test, describe)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test.Html.Selector exposing (attribute, text)
|
|
||||||
|
|
||||||
tests =
|
|
||||||
describe "attributes"
|
|
||||||
[ test "the welcome <h1> says hello!" <|
|
|
||||||
\() ->
|
|
||||||
Html.div [] [ Html.h1 [ Attr.title "greeting" ] [ Html.text "Hello!" ] ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find [ attribute <| Attr.title "greeting" ]
|
|
||||||
|> Query.has [ text "Hello!" ]
|
|
||||||
, test "the .Hello.World div has the class Hello" <|
|
|
||||||
\() ->
|
|
||||||
Html.div
|
|
||||||
[ Attr.classList
|
|
||||||
[ ( True, "Hello" )
|
|
||||||
, ( True, "World" )
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find
|
|
||||||
[ attribute <|
|
|
||||||
Attr.classList [ ( True, Hello ) ]
|
|
||||||
]
|
|
||||||
, test "the header is red" <|
|
|
||||||
\() ->
|
|
||||||
Html.header
|
|
||||||
[ Attr.style
|
|
||||||
[ ( "backround-color", "red" )
|
|
||||||
, ( "color", "yellow" )
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find
|
|
||||||
[ attribute <|
|
|
||||||
Attr.style [ ( "backround-color", "red" ) ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Releases
|
|
||||||
| Version | Notes |
|
|
||||||
| ------- | ----- |
|
|
||||||
| [**5.1.2**](https://github.com/eeue56/elm-html-test/tree/5.1.2) | Fix bug with mapped and lazy views
|
|
||||||
| [**5.1.1**](https://github.com/eeue56/elm-html-test/tree/5.1.1) | Fix children
|
|
||||||
| [**5.1.0**](https://github.com/eeue56/elm-html-test/tree/5.1.0) | Add filtermap
|
|
||||||
| [**5.0.1**](https://github.com/eeue56/elm-html-test/tree/5.0.1) | Fix bug with lazy views
|
|
||||||
| [**5.0.0**](https://github.com/eeue56/elm-html-test/tree/5.0.0) | Allow querying by attributes
|
|
||||||
| [**4.1.0**](https://github.com/eeue56/elm-html-test/tree/4.1.0) | Query styles
|
|
||||||
| [**4.0.0**](https://github.com/eeue56/elm-html-test/tree/4.0.0) | Allow custom events
|
|
||||||
| [**3.0.0**](https://github.com/eeue56/elm-html-test/tree/3.0.0) | Allow events to be testable
|
|
||||||
| [**2.0.0**](https://github.com/eeue56/elm-html-test/tree/2.0.0) | Better support for events by @rogeriochaves
|
|
||||||
| [**1.1.0**](https://github.com/eeue56/elm-html-test/tree/1.1.0) | Support for events by @rogeriochaves
|
|
||||||
| [**1.0.0**](https://github.com/eeue56/elm-html-test/tree/1.0.0) | Initial release
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5.1.2",
|
|
||||||
"summary": "Write tests for your Html.",
|
|
||||||
"repository": "https://github.com/eeue56/elm-html-test.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [
|
|
||||||
"Test.Html.Selector",
|
|
||||||
"Test.Html.Query",
|
|
||||||
"Test.Html.Event"
|
|
||||||
],
|
|
||||||
"native-modules": true,
|
|
||||||
"dependencies": {
|
|
||||||
"eeue56/elm-html-in-elm": "5.1.0 <= v < 6.0.0",
|
|
||||||
"eeue56/elm-html-query": "3.0.0 <= v < 4.0.0",
|
|
||||||
"elm-community/elm-test": "4.0.0 <= v < 5.0.0",
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- test/elm-stuff/build-artifacts
|
|
||||||
- sysconfcpus
|
|
||||||
|
|
||||||
os:
|
|
||||||
- osx
|
|
||||||
- linux
|
|
||||||
|
|
||||||
env:
|
|
||||||
matrix:
|
|
||||||
- ELM_VERSION=0.18.0 TARGET_NODE_VERSION=node
|
|
||||||
- ELM_VERSION=0.18.0 TARGET_NODE_VERSION=4.0
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- if [ ${TRAVIS_OS_NAME} == "osx" ];
|
|
||||||
then brew update; brew install nvm; mkdir ~/.nvm; export NVM_DIR=~/.nvm; source $(brew --prefix nvm)/nvm.sh;
|
|
||||||
fi
|
|
||||||
- echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
|
|
||||||
- | # epic build time improvement - see https://github.com/elm-lang/elm-compiler/issues/1473#issuecomment-245704142
|
|
||||||
if [ ! -d sysconfcpus/bin ];
|
|
||||||
then
|
|
||||||
git clone https://github.com/obmarg/libsysconfcpus.git;
|
|
||||||
cd libsysconfcpus;
|
|
||||||
./configure --prefix=$TRAVIS_BUILD_DIR/sysconfcpus;
|
|
||||||
make && make install;
|
|
||||||
cd ..;
|
|
||||||
fi
|
|
||||||
|
|
||||||
install:
|
|
||||||
- nvm install $TARGET_NODE_VERSION
|
|
||||||
- nvm use $TARGET_NODE_VERSION
|
|
||||||
- node --version
|
|
||||||
- npm --version
|
|
||||||
- cd tests
|
|
||||||
- npm install -g elm@$ELM_VERSION elm-test
|
|
||||||
- mv $(npm config get prefix)/bin/elm-make $(npm config get prefix)/bin/elm-make-old
|
|
||||||
- printf '%s\n\n' '#!/bin/bash' 'echo "Running elm-make with sysconfcpus -n 2"' '$TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-make-old "$@"' > $(npm config get prefix)/bin/elm-make
|
|
||||||
- chmod +x $(npm config get prefix)/bin/elm-make
|
|
||||||
- npm install
|
|
||||||
- elm package install --yes
|
|
||||||
|
|
||||||
script:
|
|
||||||
- elm-test
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
module ExampleApp exposing (..)
|
|
||||||
|
|
||||||
import Html exposing (..)
|
|
||||||
import Html.Attributes exposing (..)
|
|
||||||
import Html.Events exposing (onClick)
|
|
||||||
import Html.Keyed as Keyed
|
|
||||||
import Html.Lazy as Lazy
|
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
|
||||||
()
|
|
||||||
|
|
||||||
|
|
||||||
exampleModel : Model
|
|
||||||
exampleModel =
|
|
||||||
()
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= GoToHome
|
|
||||||
| GoToExamples
|
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Msg
|
|
||||||
view model =
|
|
||||||
div [ class "container" ]
|
|
||||||
[ header [ class "funky themed", id "heading" ]
|
|
||||||
[ a [ href "http://elm-lang.org", onClick GoToHome ] [ text "home" ]
|
|
||||||
, a [ href "http://elm-lang.org/examples", onClick GoToExamples ] [ text "examples" ]
|
|
||||||
, a [ href "http://elm-lang.org/docs" ] [ text "docs" ]
|
|
||||||
]
|
|
||||||
, section [ class "funky themed", id "section" ]
|
|
||||||
[ someList ]
|
|
||||||
, footer [] [ text "this is the footer" ]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
someList : Html Msg
|
|
||||||
someList =
|
|
||||||
Keyed.ul [ class "some-list" ]
|
|
||||||
[ ( "1"
|
|
||||||
, Lazy.lazy (\_ -> li [ class "list-item themed" ] [ text "first item" ])
|
|
||||||
Nothing
|
|
||||||
)
|
|
||||||
, ( "2"
|
|
||||||
, Lazy.lazy (\_ -> li [ class "list-item themed" ] [ text "second item" ])
|
|
||||||
Nothing
|
|
||||||
)
|
|
||||||
, ( "3"
|
|
||||||
, Lazy.lazy (\_ -> li [ class "list-item themed selected" ] [ text "third item" ])
|
|
||||||
Nothing
|
|
||||||
)
|
|
||||||
, ( "4"
|
|
||||||
, Lazy.lazy (\_ -> li [ class "list-item themed" ] [ text "fourth item" ])
|
|
||||||
Nothing
|
|
||||||
)
|
|
||||||
]
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
port module Main exposing (..)
|
|
||||||
|
|
||||||
import ExampleApp exposing (Msg(..), exampleModel, view)
|
|
||||||
import Expect
|
|
||||||
import Json.Encode exposing (Value)
|
|
||||||
import Test exposing (..)
|
|
||||||
import Test.Html.Event as Event
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test.Html.Selector exposing (..)
|
|
||||||
import Test.Runner.Node exposing (TestProgram, run)
|
|
||||||
|
|
||||||
|
|
||||||
main : TestProgram
|
|
||||||
main =
|
|
||||||
[ testView
|
|
||||||
]
|
|
||||||
|> Test.concat
|
|
||||||
|> run emit
|
|
||||||
|
|
||||||
|
|
||||||
port emit : ( String, Value ) -> Cmd msg
|
|
||||||
|
|
||||||
|
|
||||||
testView : Test
|
|
||||||
testView =
|
|
||||||
let
|
|
||||||
output =
|
|
||||||
view exampleModel
|
|
||||||
|> Query.fromHtml
|
|
||||||
in
|
|
||||||
describe "view exampleModel"
|
|
||||||
[ test "expect 4x <li> somewhere on the page" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.count (Expect.equal 4)
|
|
||||||
, test "expect 4x <li> inside a <ul>" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "ul" ]
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.count (Expect.equal 4)
|
|
||||||
, test "(this should fail) expect header to have 4 links in it, even though it has 3" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ id "heading" ]
|
|
||||||
|> Query.findAll [ tag "a" ]
|
|
||||||
|> Query.count (Expect.equal 4)
|
|
||||||
, test "(this should fail) expect header to have one link in it, even though it has 3" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ id "heading" ]
|
|
||||||
|> Query.find [ tag "a" ]
|
|
||||||
|> Query.has [ tag "a" ]
|
|
||||||
, test "(this should fail) expect header to have one <img> in it, even though it has none" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ id "heading" ]
|
|
||||||
|> Query.find [ tag "img" ]
|
|
||||||
|> Query.has [ tag "img" ]
|
|
||||||
, test "(this should fail) expect footer to have a child" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "footer" ]
|
|
||||||
|> Query.children []
|
|
||||||
|> Query.each (Query.has [ tag "catapult" ])
|
|
||||||
, test "(this should fail) expect footer's nonexistant child to be a catapult" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "footer" ]
|
|
||||||
|> Query.children []
|
|
||||||
|> Query.first
|
|
||||||
|> Query.has [ tag "catapult" ]
|
|
||||||
, test "expect footer to have footer text" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "footer" ]
|
|
||||||
|> Query.has [ tag "footer", text "this is the footer" ]
|
|
||||||
, test "(this should fail) expect footer to have text it doesn't have" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "footer" ]
|
|
||||||
|> Query.has [ tag "footer", text "this is SPARTA!!!" ]
|
|
||||||
, test "expect each <li> to have classes list-item and themed" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "ul" ]
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.each (Query.has [ classes [ "list-item", "themed" ] ])
|
|
||||||
, test "expect first a to send GoToHome onClick" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "a" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Events.simulate Click
|
|
||||||
|> Expect.equal (Ok GoToHome)
|
|
||||||
, test "(this should fail) expect first a to return GoToExamples on click, even though it returns GoToHome" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "a" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Events.simulate Click
|
|
||||||
|> Expect.equal (Ok GoToExamples)
|
|
||||||
, test "(this should fail) expect first a to return a msg for a blur event, even though it doesn't have one" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "a" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Events.simulate Blur
|
|
||||||
|> Expect.equal (Ok GoToHome)
|
|
||||||
, test "(this should fail) expect text to return a msg for click, even though it is a text" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ text "home" ]
|
|
||||||
|> Events.simulate Click
|
|
||||||
|> Expect.equal (Ok GoToHome)
|
|
||||||
]
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "helpful summary of your project, less than 80 characters",
|
|
||||||
"repository": "https://github.com/eeue56/elm-html-test.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
".",
|
|
||||||
"../src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"native-modules": true,
|
|
||||||
"dependencies": {
|
|
||||||
"eeue56/elm-html-in-elm": "3.0.0 <= v < 4.0.0",
|
|
||||||
"eeue56/elm-html-query": "1.1.0 <= v < 2.0.0",
|
|
||||||
"elm-community/elm-test": "3.1.0 <= v < 4.0.0",
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
|
||||||
"rtfeldman/html-test-runner": "2.0.0 <= v < 3.0.0",
|
|
||||||
"rtfeldman/node-test-runner": "3.0.0 <= v < 4.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
module Html.Inert exposing (Node, parseAttribute, fromElmHtml, fromHtml, toElmHtml)
|
|
||||||
|
|
||||||
{-| Inert Html - that is, can't do anything with events.
|
|
||||||
-}
|
|
||||||
|
|
||||||
import ElmHtml.InternalTypes exposing (ElmHtml(..), EventHandler, Facts, Tagger, decodeElmHtml, decodeAttribute)
|
|
||||||
import Html exposing (Html)
|
|
||||||
import Json.Decode
|
|
||||||
import Native.HtmlAsJson
|
|
||||||
|
|
||||||
|
|
||||||
type Node msg
|
|
||||||
= Node (ElmHtml msg)
|
|
||||||
|
|
||||||
|
|
||||||
fromHtml : Html msg -> Node msg
|
|
||||||
fromHtml html =
|
|
||||||
case Json.Decode.decodeValue (decodeElmHtml taggedEventDecoder) (toJson html) of
|
|
||||||
Ok elmHtml ->
|
|
||||||
Node elmHtml
|
|
||||||
|
|
||||||
Err str ->
|
|
||||||
Debug.crash ("Error internally processing HTML for testing - please report this error message as a bug: " ++ str)
|
|
||||||
|
|
||||||
|
|
||||||
fromElmHtml : ElmHtml msg -> Node msg
|
|
||||||
fromElmHtml =
|
|
||||||
Node
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a Html node to a Json string
|
|
||||||
-}
|
|
||||||
toJson : Html a -> Json.Decode.Value
|
|
||||||
toJson node =
|
|
||||||
Native.HtmlAsJson.toJson node
|
|
||||||
|
|
||||||
|
|
||||||
toElmHtml : Node msg -> ElmHtml msg
|
|
||||||
toElmHtml (Node elmHtml) =
|
|
||||||
elmHtml
|
|
||||||
|
|
||||||
|
|
||||||
impossibleMessage : String
|
|
||||||
impossibleMessage =
|
|
||||||
"An Inert Node fired an event handler. This should never happen! Please report this bug."
|
|
||||||
|
|
||||||
|
|
||||||
attributeToJson : Html.Attribute a -> Json.Decode.Value
|
|
||||||
attributeToJson attribute =
|
|
||||||
Native.HtmlAsJson.attributeToJson attribute
|
|
||||||
|
|
||||||
|
|
||||||
parseAttribute : Html.Attribute a -> ElmHtml.InternalTypes.Attribute
|
|
||||||
parseAttribute attr =
|
|
||||||
case Json.Decode.decodeValue decodeAttribute (attributeToJson attr) of
|
|
||||||
Ok parsedAttribute ->
|
|
||||||
parsedAttribute
|
|
||||||
|
|
||||||
Err str ->
|
|
||||||
Debug.crash ("Error internally processing Attribute for testing - please report this error message as a bug: " ++ str)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Gets the function out of a tagger
|
|
||||||
-}
|
|
||||||
taggerFunction : Tagger -> (a -> msg)
|
|
||||||
taggerFunction tagger =
|
|
||||||
Native.HtmlAsJson.taggerFunction tagger
|
|
||||||
|
|
||||||
|
|
||||||
{-| Gets the decoder out of an EventHandler
|
|
||||||
-}
|
|
||||||
eventDecoder : EventHandler -> Json.Decode.Decoder msg
|
|
||||||
eventDecoder eventHandler =
|
|
||||||
Native.HtmlAsJson.eventDecoder eventHandler
|
|
||||||
|
|
||||||
|
|
||||||
{-| Applies the taggers over the event handlers to have the complete event decoder
|
|
||||||
-}
|
|
||||||
taggedEventDecoder : List Tagger -> EventHandler -> Json.Decode.Decoder msg
|
|
||||||
taggedEventDecoder taggers eventHandler =
|
|
||||||
case taggers of
|
|
||||||
[] ->
|
|
||||||
eventDecoder eventHandler
|
|
||||||
|
|
||||||
[ tagger ] ->
|
|
||||||
Json.Decode.map (taggerFunction tagger) (eventDecoder eventHandler)
|
|
||||||
|
|
||||||
tagger :: taggers ->
|
|
||||||
Json.Decode.map (taggerFunction tagger) (taggedEventDecoder taggers eventHandler)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
var _eeue56$elm_html_test$Native_HtmlAsJson = (function() {
|
|
||||||
function forceThunks(vNode) {
|
|
||||||
if (typeof vNode !== "undefined" && vNode.ctor === "_Tuple2" && !vNode.node) {
|
|
||||||
vNode._1 = forceThunks(vNode._1);
|
|
||||||
}
|
|
||||||
if (typeof vNode !== 'undefined' && vNode.type === 'thunk' && !vNode.node) {
|
|
||||||
vNode.node = vNode.thunk.apply(vNode.thunk, vNode.args);
|
|
||||||
}
|
|
||||||
if (typeof vNode !== 'undefined' && vNode.type === 'tagger') {
|
|
||||||
vNode.node = forceThunks(vNode.node);
|
|
||||||
}
|
|
||||||
if (typeof vNode !== 'undefined' && typeof vNode.children !== 'undefined') {
|
|
||||||
vNode.children = vNode.children.map(forceThunks);
|
|
||||||
}
|
|
||||||
return vNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
toJson: function(html) {
|
|
||||||
return forceThunks(html);
|
|
||||||
},
|
|
||||||
eventDecoder: function (event) {
|
|
||||||
return event.decoder;
|
|
||||||
},
|
|
||||||
taggerFunction: function (tagger) {
|
|
||||||
return tagger;
|
|
||||||
},
|
|
||||||
attributeToJson: function(attribute) {
|
|
||||||
return attribute;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
module Test.Html.Descendant exposing (isDescendant)
|
|
||||||
|
|
||||||
import Html exposing (Html)
|
|
||||||
import ElmHtml.InternalTypes exposing (ElmHtml(..))
|
|
||||||
import Html.Inert exposing (fromHtml, toElmHtml)
|
|
||||||
|
|
||||||
|
|
||||||
isDescendant : List (ElmHtml msg) -> ElmHtml msg -> Bool
|
|
||||||
isDescendant html potentialDescendant =
|
|
||||||
case html of
|
|
||||||
[] ->
|
|
||||||
False
|
|
||||||
|
|
||||||
current :: rest ->
|
|
||||||
if current == potentialDescendant then
|
|
||||||
True
|
|
||||||
else
|
|
||||||
isDescendant
|
|
||||||
(prependChildren current rest)
|
|
||||||
potentialDescendant
|
|
||||||
|
|
||||||
|
|
||||||
prependChildren : ElmHtml msg -> List (ElmHtml msg) -> List (ElmHtml msg)
|
|
||||||
prependChildren parentNode nodeList =
|
|
||||||
case parentNode of
|
|
||||||
NodeEntry { children } ->
|
|
||||||
(List.concat [ children, nodeList ])
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
nodeList
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
module Test.Html.Event
|
|
||||||
exposing
|
|
||||||
( Event
|
|
||||||
, simulate
|
|
||||||
, expect
|
|
||||||
, toResult
|
|
||||||
, click
|
|
||||||
, doubleClick
|
|
||||||
, mouseDown
|
|
||||||
, mouseUp
|
|
||||||
, mouseEnter
|
|
||||||
, mouseLeave
|
|
||||||
, mouseOver
|
|
||||||
, mouseOut
|
|
||||||
, input
|
|
||||||
, check
|
|
||||||
, submit
|
|
||||||
, blur
|
|
||||||
, focus
|
|
||||||
, custom
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| This module lets you simulate events on `Html` values and expect that
|
|
||||||
they result in certain `Msg` values being sent to `update`.
|
|
||||||
|
|
||||||
|
|
||||||
## Simulating Events
|
|
||||||
|
|
||||||
@docs Event, simulate, expect, toResult
|
|
||||||
|
|
||||||
|
|
||||||
## Event Builders
|
|
||||||
|
|
||||||
@docs custom, click, doubleClick, mouseDown, mouseUp, mouseEnter, mouseLeave, mouseOver, mouseOut, input, check, submit, blur, focus
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Dict
|
|
||||||
import ElmHtml.InternalTypes exposing (ElmHtml, ElmHtml(..), Tagger)
|
|
||||||
import Json.Decode as Decode exposing (Decoder)
|
|
||||||
import Json.Encode as Encode exposing (Value)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test.Html.Query.Internal as QueryInternal
|
|
||||||
import Expect exposing (Expectation)
|
|
||||||
|
|
||||||
|
|
||||||
{-| A simulated event.
|
|
||||||
|
|
||||||
See [`simulate`](#simulate).
|
|
||||||
|
|
||||||
-}
|
|
||||||
type Event msg
|
|
||||||
= Event ( String, Value ) (QueryInternal.Single msg)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Simulate an event on a node.
|
|
||||||
|
|
||||||
import Test.Html.Event as Event
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= Change String
|
|
||||||
|
|
||||||
|
|
||||||
test "Input produces expected Msg" <|
|
|
||||||
\() ->
|
|
||||||
Html.input [ onInput Change ] [ ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Event.simulate (Event.input "cats")
|
|
||||||
|> Event.expect (Change "cats")
|
|
||||||
|
|
||||||
-}
|
|
||||||
simulate : ( String, Value ) -> Query.Single msg -> Event msg
|
|
||||||
simulate =
|
|
||||||
Event
|
|
||||||
|
|
||||||
|
|
||||||
{-| Passes if the given message is triggered by the simulated event.
|
|
||||||
|
|
||||||
import Test.Html.Event as Event
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= Change String
|
|
||||||
|
|
||||||
|
|
||||||
test "Input produces expected Msg" <|
|
|
||||||
\() ->
|
|
||||||
Html.input [ onInput Change ] [ ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Event.simulate (Event.input "cats")
|
|
||||||
|> Event.expect (Change "cats")
|
|
||||||
|
|
||||||
-}
|
|
||||||
expect : msg -> Event msg -> Expectation
|
|
||||||
expect msg (Event event (QueryInternal.Single showTrace query)) =
|
|
||||||
case toResult (Event event (QueryInternal.Single showTrace query)) of
|
|
||||||
Err noEvent ->
|
|
||||||
Expect.fail noEvent
|
|
||||||
|> QueryInternal.failWithQuery showTrace "" query
|
|
||||||
|
|
||||||
Ok foundMsg ->
|
|
||||||
foundMsg
|
|
||||||
|> Expect.equal msg
|
|
||||||
|> QueryInternal.failWithQuery showTrace ("Event.expectEvent: Expected the msg \x1B[32m" ++ toString msg ++ "\x1B[39m from the event \x1B[31m" ++ toString event ++ "\x1B[39m but could not find the event.") query
|
|
||||||
|
|
||||||
|
|
||||||
{-| Returns a Result with the Msg produced by the event simulated on a node.
|
|
||||||
Note that Event.expect gives nicer messages; this is generally more useful
|
|
||||||
when testing that an event handler is *not* present.
|
|
||||||
|
|
||||||
import Test.Html.Event as Event
|
|
||||||
|
|
||||||
|
|
||||||
test "Input produces expected Msg" <|
|
|
||||||
\() ->
|
|
||||||
Html.input [ onInput Change ] [ ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Event.simulate (Event.input "cats")
|
|
||||||
|> Event.toResult
|
|
||||||
|> Expect.equal (Ok (Change "cats"))
|
|
||||||
|
|
||||||
-}
|
|
||||||
toResult : Event msg -> Result String msg
|
|
||||||
toResult (Event ( eventName, jsEvent ) (QueryInternal.Single showTrace query)) =
|
|
||||||
let
|
|
||||||
node =
|
|
||||||
QueryInternal.traverse query
|
|
||||||
|> Result.andThen (QueryInternal.verifySingle eventName)
|
|
||||||
|> Result.mapError (QueryInternal.queryErrorToString query)
|
|
||||||
in
|
|
||||||
case node of
|
|
||||||
Err msg ->
|
|
||||||
Err msg
|
|
||||||
|
|
||||||
Ok single ->
|
|
||||||
findEvent eventName single
|
|
||||||
|> Result.andThen (\foundEvent -> Decode.decodeValue foundEvent jsEvent)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- EVENTS --
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`click`](https://developer.mozilla.org/en-US/docs/Web/Events/click) event.
|
|
||||||
-}
|
|
||||||
click : ( String, Value )
|
|
||||||
click =
|
|
||||||
( "click", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`dblclick`](https://developer.mozilla.org/en-US/docs/Web/Events/dblclick) event.
|
|
||||||
-}
|
|
||||||
doubleClick : ( String, Value )
|
|
||||||
doubleClick =
|
|
||||||
( "dblclick", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`mousedown`](https://developer.mozilla.org/en-US/docs/Web/Events/mousedown) event.
|
|
||||||
-}
|
|
||||||
mouseDown : ( String, Value )
|
|
||||||
mouseDown =
|
|
||||||
( "mousedown", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`mouseup`](https://developer.mozilla.org/en-US/docs/Web/Events/mouseup) event.
|
|
||||||
-}
|
|
||||||
mouseUp : ( String, Value )
|
|
||||||
mouseUp =
|
|
||||||
( "mouseup", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`mouseenter`](https://developer.mozilla.org/en-US/docs/Web/Events/mouseenter) event.
|
|
||||||
-}
|
|
||||||
mouseEnter : ( String, Value )
|
|
||||||
mouseEnter =
|
|
||||||
( "mouseenter", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`mouseleave`](https://developer.mozilla.org/en-US/docs/Web/Events/mouseleave) event.
|
|
||||||
-}
|
|
||||||
mouseLeave : ( String, Value )
|
|
||||||
mouseLeave =
|
|
||||||
( "mouseleave", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`mouseover`](https://developer.mozilla.org/en-US/docs/Web/Events/mouseover) event.
|
|
||||||
-}
|
|
||||||
mouseOver : ( String, Value )
|
|
||||||
mouseOver =
|
|
||||||
( "mouseover", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`mouseout`](https://developer.mozilla.org/en-US/docs/Web/Events/mouseout) event.
|
|
||||||
-}
|
|
||||||
mouseOut : ( String, Value )
|
|
||||||
mouseOut =
|
|
||||||
( "mouseout", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| An [`input`](https://developer.mozilla.org/en-US/docs/Web/Events/input) event.
|
|
||||||
-}
|
|
||||||
input : String -> ( String, Value )
|
|
||||||
input value =
|
|
||||||
( "input"
|
|
||||||
, Encode.object
|
|
||||||
[ ( "target"
|
|
||||||
, Encode.object [ ( "value", Encode.string value ) ]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`change`](https://developer.mozilla.org/en-US/docs/Web/Events/change) event
|
|
||||||
where `event.target.checked` is set to the given `Bool` value.
|
|
||||||
-}
|
|
||||||
check : Bool -> ( String, Value )
|
|
||||||
check checked =
|
|
||||||
( "change"
|
|
||||||
, Encode.object
|
|
||||||
[ ( "target"
|
|
||||||
, Encode.object [ ( "checked", Encode.bool checked ) ]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`submit`](https://developer.mozilla.org/en-US/docs/Web/Events/submit) event.
|
|
||||||
-}
|
|
||||||
submit : ( String, Value )
|
|
||||||
submit =
|
|
||||||
( "submit", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`blur`](https://developer.mozilla.org/en-US/docs/Web/Events/blur) event.
|
|
||||||
-}
|
|
||||||
blur : ( String, Value )
|
|
||||||
blur =
|
|
||||||
( "blur", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| A [`focus`](https://developer.mozilla.org/en-US/docs/Web/Events/focus) event.
|
|
||||||
-}
|
|
||||||
focus : ( String, Value )
|
|
||||||
focus =
|
|
||||||
( "focus", emptyObject )
|
|
||||||
|
|
||||||
|
|
||||||
{-| 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.
|
|
||||||
|
|
||||||
import Test.Html.Event as Event
|
|
||||||
import Json.Encode as Encode exposing (Value)
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= Change String
|
|
||||||
|
|
||||||
|
|
||||||
test "Input produces expected Msg" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
simulatedEventObject : Value
|
|
||||||
simulatedEventObject =
|
|
||||||
Encode.object
|
|
||||||
[ ( "target"
|
|
||||||
, Encode.object [ ( "value", Encode.string "cats" ) ]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
in
|
|
||||||
Html.input [ onInput Change ] [ ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Event.simulate (Event.custom "input" simulatedEventObject)
|
|
||||||
|> Event.expect (Change "cats")
|
|
||||||
|
|
||||||
-}
|
|
||||||
custom : String -> Value -> ( String, Value )
|
|
||||||
custom =
|
|
||||||
(,)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- INTERNAL --
|
|
||||||
|
|
||||||
|
|
||||||
emptyObject : Value
|
|
||||||
emptyObject =
|
|
||||||
Encode.object []
|
|
||||||
|
|
||||||
|
|
||||||
findEvent : String -> ElmHtml msg -> Result String (Decoder msg)
|
|
||||||
findEvent eventName element =
|
|
||||||
let
|
|
||||||
elementOutput =
|
|
||||||
QueryInternal.prettyPrint element
|
|
||||||
|
|
||||||
eventDecoder node =
|
|
||||||
node.facts.events
|
|
||||||
|> Dict.get eventName
|
|
||||||
|> Result.fromMaybe ("Event.expectEvent: The event " ++ eventName ++ " does not exist on the found node.\n\n" ++ elementOutput)
|
|
||||||
in
|
|
||||||
case element of
|
|
||||||
TextTag _ ->
|
|
||||||
Err ("Found element is a text, which does not produce events, therefore could not simulate " ++ eventName ++ " on it. Text found: " ++ elementOutput)
|
|
||||||
|
|
||||||
NodeEntry node ->
|
|
||||||
eventDecoder node
|
|
||||||
|
|
||||||
CustomNode node ->
|
|
||||||
eventDecoder node
|
|
||||||
|
|
||||||
MarkdownNode node ->
|
|
||||||
eventDecoder node
|
|
||||||
|
|
||||||
NoOp ->
|
|
||||||
Err ("Unknown element found. Could not simulate " ++ eventName ++ " on it.")
|
|
||||||
@@ -1,471 +0,0 @@
|
|||||||
module Test.Html.Query
|
|
||||||
exposing
|
|
||||||
( Multiple
|
|
||||||
, Single
|
|
||||||
, children
|
|
||||||
, contains
|
|
||||||
, count
|
|
||||||
, each
|
|
||||||
, find
|
|
||||||
, findAll
|
|
||||||
, first
|
|
||||||
, fromHtml
|
|
||||||
, has
|
|
||||||
, hasNot
|
|
||||||
, index
|
|
||||||
, keep
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| Querying HTML structure.
|
|
||||||
|
|
||||||
@docs Single, Multiple, fromHtml
|
|
||||||
|
|
||||||
|
|
||||||
## Querying
|
|
||||||
|
|
||||||
@docs find, findAll, children, first, index, keep
|
|
||||||
|
|
||||||
|
|
||||||
## Expecting
|
|
||||||
|
|
||||||
@docs count, contains, has, hasNot, each
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import ElmHtml.InternalTypes exposing (ElmHtml)
|
|
||||||
import Expect exposing (Expectation)
|
|
||||||
import Html exposing (Html)
|
|
||||||
import Html.Inert as Inert
|
|
||||||
import Test.Html.Query.Internal as Internal exposing (QueryError(..), failWithQuery)
|
|
||||||
import Test.Html.Selector.Internal as Selector exposing (Selector, selectorToString)
|
|
||||||
|
|
||||||
|
|
||||||
{- DESIGN NOTES:
|
|
||||||
|
|
||||||
The reason for having `Query.index` and `Query.first` instead of doing them as
|
|
||||||
selectors (which would let you do e.g. `Query.find [ first ]` to get the
|
|
||||||
first child, instead of `Query.children [] |> Query.first` like you have to
|
|
||||||
do now) is that it's not immediately obvious what a query like this would do:
|
|
||||||
|
|
||||||
Query.findAll [ first, tag "li" ]
|
|
||||||
|
|
||||||
Is that getting the first descendant, and then checking whether it's an <li>?
|
|
||||||
Or is it finding the first <li> descendant? (Yes.) Also this is a findAll
|
|
||||||
but it's only ever returning a single result despite being typed as a Multiple.
|
|
||||||
|
|
||||||
Arguably `id` could be treated the same way - since you *should* only have
|
|
||||||
one id, *should* only ever return one result. However, in that case, it's
|
|
||||||
possible that you have multiple IDs - and in that case you actually want the
|
|
||||||
test to fail so you find out about the mistake!
|
|
||||||
-}
|
|
||||||
|
|
||||||
|
|
||||||
{-| A query that expects to find exactly one element.
|
|
||||||
|
|
||||||
Contrast with [`Multiple`](#Multiple).
|
|
||||||
|
|
||||||
-}
|
|
||||||
type alias Single msg =
|
|
||||||
Internal.Single msg
|
|
||||||
|
|
||||||
|
|
||||||
{-| A query that may find any number of elements, including zero.
|
|
||||||
|
|
||||||
Contrast with [`Single`](#Single).
|
|
||||||
|
|
||||||
-}
|
|
||||||
type alias Multiple msg =
|
|
||||||
Internal.Multiple msg
|
|
||||||
|
|
||||||
|
|
||||||
{-| Translate a `Html` value into a `Single` query. This is how queries
|
|
||||||
typically begin.
|
|
||||||
|
|
||||||
import Html
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (text)
|
|
||||||
|
|
||||||
|
|
||||||
test "Button has the expected text" <|
|
|
||||||
\() ->
|
|
||||||
Html.button [] [ Html.text "I'm a button!" ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.has [ text "I'm a button!" ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
fromHtml : Html msg -> Single msg
|
|
||||||
fromHtml html =
|
|
||||||
Internal.Query (Inert.fromHtml html) []
|
|
||||||
|> Internal.Single True
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- TRAVERSAL --
|
|
||||||
|
|
||||||
|
|
||||||
{-| Find the descendant elements which match all the given selectors.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag)
|
|
||||||
import Expect
|
|
||||||
|
|
||||||
|
|
||||||
test "The list has three items" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ text "first item" ]
|
|
||||||
, li [] [ text "second item" ]
|
|
||||||
, li [] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.count (Expect.equal 3)
|
|
||||||
|
|
||||||
-}
|
|
||||||
findAll : List Selector -> Single msg -> Multiple msg
|
|
||||||
findAll selectors (Internal.Single showTrace query) =
|
|
||||||
Internal.FindAll selectors
|
|
||||||
|> Internal.prependSelector query
|
|
||||||
|> Internal.Multiple showTrace
|
|
||||||
|
|
||||||
|
|
||||||
{-| Find the descendant elements of the result of `findAll` which match all the given selectors.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag)
|
|
||||||
import Expect
|
|
||||||
|
|
||||||
|
|
||||||
test "The list has three items" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ a [] [ text "first item" ]]
|
|
||||||
, li [] [ a [] [ text "second item" ]]
|
|
||||||
, li [] [ a [] [ text "third item" ]]
|
|
||||||
, li [] [ button [] [ text "button" ]]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.keep ( tag "a" )
|
|
||||||
|> Expect.all
|
|
||||||
[ Query.each (Query.has [ tag "a" ])
|
|
||||||
, Query.first >> Query.has [ text "first item" ]
|
|
||||||
]
|
|
||||||
|
|
||||||
-}
|
|
||||||
keep : Selector -> Multiple msg -> Multiple msg
|
|
||||||
keep selector (Internal.Multiple showTrace query) =
|
|
||||||
Internal.FindAll [ selector ]
|
|
||||||
|> Internal.prependSelector query
|
|
||||||
|> Internal.Multiple showTrace
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return the matched element's immediate child elements.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "The <ul> only has <li> children" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [ class "item"] [ text "first item" ]
|
|
||||||
, li [ class "item selected"] [ text "second item" ]
|
|
||||||
, li [ class "item"] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find [ class "items" ]
|
|
||||||
|> Query.children [ class "selected" ]
|
|
||||||
|> Query.count (Expect.equal 1)
|
|
||||||
|
|
||||||
-}
|
|
||||||
children : List Selector -> Single msg -> Multiple msg
|
|
||||||
children selectors (Internal.Single showTrace query) =
|
|
||||||
Internal.Children selectors
|
|
||||||
|> Internal.prependSelector query
|
|
||||||
|> Internal.Multiple showTrace
|
|
||||||
|
|
||||||
|
|
||||||
{-| Find exactly one descendant element which matches all the given selectors.
|
|
||||||
If no descendants match, or if more than one matches, the test will fail.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "The list has both the classes 'items' and 'active'" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ text "first item" ]
|
|
||||||
, li [] [ text "second item" ]
|
|
||||||
, li [] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find [ tag "ul" ]
|
|
||||||
|> Query.has [ classes [ "items", "active" ] ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
find : List Selector -> Single msg -> Single msg
|
|
||||||
find selectors (Internal.Single showTrace query) =
|
|
||||||
Internal.Find selectors
|
|
||||||
|> Internal.prependSelector query
|
|
||||||
|> Internal.Single showTrace
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return the first element in a match. If there were no matches, the test
|
|
||||||
will fail.
|
|
||||||
|
|
||||||
`Query.first` is a shorthand for `Query.index 0` - they do the same thing.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "The first <li> is called 'first item'" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ text "first item" ]
|
|
||||||
, li [] [ text "second item" ]
|
|
||||||
, li [] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Query.has [ text "first item" ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
first : Multiple msg -> Single msg
|
|
||||||
first (Internal.Multiple showTrace query) =
|
|
||||||
Internal.First
|
|
||||||
|> Internal.prependSelector query
|
|
||||||
|> Internal.Single showTrace
|
|
||||||
|
|
||||||
|
|
||||||
{-| Return the element in a match at the given index. For example,
|
|
||||||
`Query.index 0` would match the first element, and `Query.index 1` would match
|
|
||||||
the second element.
|
|
||||||
|
|
||||||
You can pass negative numbers to "wrap around" - for example, `Query.index -1`
|
|
||||||
will match the last element, and `Query.index -2` will match the second-to-last.
|
|
||||||
|
|
||||||
If the index falls outside the bounds of the match, the test will fail.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "The second <li> is called 'second item'" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ text "first item" ]
|
|
||||||
, li [] [ text "second item" ]
|
|
||||||
, li [] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.index 1
|
|
||||||
|> Query.has [ text "second item" ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
index : Int -> Multiple msg -> Single msg
|
|
||||||
index position (Internal.Multiple showTrace query) =
|
|
||||||
Internal.Index position
|
|
||||||
|> Internal.prependSelector query
|
|
||||||
|> Internal.Single showTrace
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- EXPECTATIONS --
|
|
||||||
|
|
||||||
|
|
||||||
{-| Expect the number of elements matching the query fits the given expectation.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag)
|
|
||||||
import Expect
|
|
||||||
|
|
||||||
|
|
||||||
test "The list has three items" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ text "first item" ]
|
|
||||||
, li [] [ text "second item" ]
|
|
||||||
, li [] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.count (Expect.equal 3)
|
|
||||||
|
|
||||||
-}
|
|
||||||
count : (Int -> Expectation) -> Multiple msg -> Expectation
|
|
||||||
count expect ((Internal.Multiple showTrace query) as multiple) =
|
|
||||||
(List.length >> expect >> failWithQuery showTrace "Query.count" query)
|
|
||||||
|> Internal.multipleToExpectation multiple
|
|
||||||
|
|
||||||
|
|
||||||
{-| Expect the element to have at least one descendant matching
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "The list has two li: one with the text \"third item\" and \
|
|
||||||
another one with \"first item\"" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ text "first item" ]
|
|
||||||
, li [] [ text "second item" ]
|
|
||||||
, li [] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.contains
|
|
||||||
[ li [] [ text "third item" ]
|
|
||||||
, li [] [ text "first item" ]
|
|
||||||
]
|
|
||||||
|
|
||||||
-}
|
|
||||||
contains : List (Html msg) -> Single msg -> Expectation
|
|
||||||
contains expectedHtml (Internal.Single showTrace query) =
|
|
||||||
let
|
|
||||||
expectedElmHtml =
|
|
||||||
List.map htmlToElm expectedHtml
|
|
||||||
in
|
|
||||||
Internal.contains
|
|
||||||
expectedElmHtml
|
|
||||||
query
|
|
||||||
|> failWithQuery showTrace "Query.contains" query
|
|
||||||
|
|
||||||
|
|
||||||
htmlToElm : Html msg -> ElmHtml msg
|
|
||||||
htmlToElm =
|
|
||||||
Inert.fromHtml >> Inert.toElmHtml
|
|
||||||
|
|
||||||
|
|
||||||
{-| Expect the element to match all of the given selectors.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "The list has both the classes 'items' and 'active'" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ text "first item" ]
|
|
||||||
, li [] [ text "second item" ]
|
|
||||||
, li [] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find [ tag "ul" ]
|
|
||||||
|> Query.has [ tag "ul", classes [ "items", "active" ] ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
has : List Selector -> Single msg -> Expectation
|
|
||||||
has selectors (Internal.Single showTrace query) =
|
|
||||||
Internal.has selectors query
|
|
||||||
|> failWithQuery showTrace ("Query.has " ++ Internal.joinAsList selectorToString selectors) query
|
|
||||||
|
|
||||||
|
|
||||||
{-| Expect the element to **not** match all of the given selectors.
|
|
||||||
|
|
||||||
import Html exposing (div)
|
|
||||||
import Html.Attributes as Attributes
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, class)
|
|
||||||
|
|
||||||
|
|
||||||
test "The div element has no progress-bar class" <|
|
|
||||||
\() ->
|
|
||||||
div [ Attributes.class "button" ] []
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find [ tag "div" ]
|
|
||||||
|> Query.hasNot [ tag "div", class "progress-bar" ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
hasNot : List Selector -> Single msg -> Expectation
|
|
||||||
hasNot selectors (Internal.Single showTrace query) =
|
|
||||||
let
|
|
||||||
queryName =
|
|
||||||
"Query.hasNot " ++ Internal.joinAsList selectorToString selectors
|
|
||||||
in
|
|
||||||
Internal.hasNot selectors query
|
|
||||||
|> failWithQuery showTrace queryName query
|
|
||||||
|
|
||||||
|
|
||||||
{-| Expect that a [`Single`](#Single) expectation will hold true for each of the
|
|
||||||
[`Multiple`](#Multiple) matched elements.
|
|
||||||
|
|
||||||
import Html exposing (div, ul, li)
|
|
||||||
import Html.Attributes exposing (class)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "The list has both the classes 'items' and 'active'" <|
|
|
||||||
\() ->
|
|
||||||
div []
|
|
||||||
[ ul [ class "items active" ]
|
|
||||||
[ li [] [ text "first item" ]
|
|
||||||
, li [] [ text "second item" ]
|
|
||||||
, li [] [ text "third item" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.findAll [ tag "ul" ]
|
|
||||||
|> Query.each
|
|
||||||
[ Query.has [ tag "ul" ]
|
|
||||||
, Query.has [ classes [ "items", "active" ] ]
|
|
||||||
]
|
|
||||||
|
|
||||||
-}
|
|
||||||
each : (Single msg -> Expectation) -> Multiple msg -> Expectation
|
|
||||||
each check (Internal.Multiple showTrace query) =
|
|
||||||
Internal.expectAll check query
|
|
||||||
|> failWithQuery showTrace "Query.each" query
|
|
||||||
@@ -1,559 +0,0 @@
|
|||||||
module Test.Html.Query.Internal exposing (..)
|
|
||||||
|
|
||||||
import ElmHtml.InternalTypes exposing (ElmHtml(..))
|
|
||||||
import ElmHtml.ToString exposing (nodeToStringWithOptions)
|
|
||||||
import Expect exposing (Expectation)
|
|
||||||
import Html.Inert as Inert exposing (Node)
|
|
||||||
import Test.Html.Descendant as Descendant
|
|
||||||
import Test.Html.Selector.Internal as InternalSelector exposing (Selector, selectorToString)
|
|
||||||
import Test.Runner
|
|
||||||
|
|
||||||
|
|
||||||
{-| Note: the selectors are stored in reverse order for better prepending perf.
|
|
||||||
-}
|
|
||||||
type Query msg
|
|
||||||
= Query (Inert.Node msg) (List SelectorQuery)
|
|
||||||
|
|
||||||
|
|
||||||
type SelectorQuery
|
|
||||||
= Find (List Selector)
|
|
||||||
| FindAll (List Selector)
|
|
||||||
| Children (List Selector)
|
|
||||||
-- First and Index are separate so we can report Query.first in error messages
|
|
||||||
| First
|
|
||||||
| Index Int
|
|
||||||
|
|
||||||
|
|
||||||
{-| The Bool is `showTrace` - whether to show the Query.fromHtml trace at
|
|
||||||
the beginning of the error message.
|
|
||||||
|
|
||||||
We need to track this so that Query.each can turn it off. Otherwise you get
|
|
||||||
fromHtml printed twice - once at the very top, then again for the nested
|
|
||||||
expectation that Query.each delegated to.
|
|
||||||
|
|
||||||
-}
|
|
||||||
type Single msg
|
|
||||||
= Single Bool (Query msg)
|
|
||||||
|
|
||||||
|
|
||||||
{-| The Bool is `showTrace` - see `Single` for more info.
|
|
||||||
-}
|
|
||||||
type Multiple msg
|
|
||||||
= Multiple Bool (Query msg)
|
|
||||||
|
|
||||||
|
|
||||||
type QueryError
|
|
||||||
= NoResultsForSingle String
|
|
||||||
| MultipleResultsForSingle String Int
|
|
||||||
|
|
||||||
|
|
||||||
toLines : String -> Query msg -> String -> List String
|
|
||||||
toLines expectationFailure (Query node selectors) queryName =
|
|
||||||
toLinesHelp expectationFailure [ Inert.toElmHtml node ] (List.reverse selectors) queryName []
|
|
||||||
|> List.reverse
|
|
||||||
|
|
||||||
|
|
||||||
prettyPrint : ElmHtml msg -> String
|
|
||||||
prettyPrint =
|
|
||||||
nodeToStringWithOptions { indent = 4, newLines = True }
|
|
||||||
|
|
||||||
|
|
||||||
toOutputLine : Query msg -> String
|
|
||||||
toOutputLine (Query node selectors) =
|
|
||||||
prettyPrint (Inert.toElmHtml node)
|
|
||||||
|
|
||||||
|
|
||||||
toLinesHelp : String -> List (ElmHtml msg) -> List SelectorQuery -> String -> List String -> List String
|
|
||||||
toLinesHelp expectationFailure elmHtmlList selectorQueries queryName results =
|
|
||||||
let
|
|
||||||
bailOut result =
|
|
||||||
-- Bail out early so the last error message the user
|
|
||||||
-- sees is Query.find rather than something like
|
|
||||||
-- Query.has, to reflect how we didn't make it that far.
|
|
||||||
String.join "\n\n\n✗ " [ result, expectationFailure ] :: results
|
|
||||||
|
|
||||||
recurse newElmHtmlList rest result =
|
|
||||||
toLinesHelp
|
|
||||||
expectationFailure
|
|
||||||
newElmHtmlList
|
|
||||||
rest
|
|
||||||
queryName
|
|
||||||
(result :: results)
|
|
||||||
in
|
|
||||||
case selectorQueries of
|
|
||||||
[] ->
|
|
||||||
String.join "\n\n" [ queryName, expectationFailure ] :: results
|
|
||||||
|
|
||||||
selectorQuery :: rest ->
|
|
||||||
case selectorQuery of
|
|
||||||
FindAll selectors ->
|
|
||||||
let
|
|
||||||
elements =
|
|
||||||
elmHtmlList
|
|
||||||
|> List.concatMap getChildren
|
|
||||||
|> InternalSelector.queryAll selectors
|
|
||||||
in
|
|
||||||
("Query.findAll " ++ joinAsList selectorToString selectors)
|
|
||||||
|> withHtmlContext (getHtmlContext elements)
|
|
||||||
|> recurse elements rest
|
|
||||||
|
|
||||||
Find selectors ->
|
|
||||||
let
|
|
||||||
elements =
|
|
||||||
elmHtmlList
|
|
||||||
|> List.concatMap getChildren
|
|
||||||
|> InternalSelector.queryAll selectors
|
|
||||||
|
|
||||||
result =
|
|
||||||
("Query.find " ++ joinAsList selectorToString selectors)
|
|
||||||
|> withHtmlContext (getHtmlContext elements)
|
|
||||||
in
|
|
||||||
if List.length elements == 1 then
|
|
||||||
recurse elements rest result
|
|
||||||
else
|
|
||||||
bailOut result
|
|
||||||
|
|
||||||
Children selectors ->
|
|
||||||
let
|
|
||||||
elements =
|
|
||||||
elmHtmlList
|
|
||||||
|> List.concatMap getChildren
|
|
||||||
|> InternalSelector.queryAllChildren selectors
|
|
||||||
in
|
|
||||||
("Query.children " ++ joinAsList selectorToString selectors)
|
|
||||||
|> withHtmlContext (getHtmlContext elements)
|
|
||||||
|> recurse elements rest
|
|
||||||
|
|
||||||
First ->
|
|
||||||
let
|
|
||||||
elements =
|
|
||||||
elmHtmlList
|
|
||||||
|> List.head
|
|
||||||
|> Maybe.map (\elem -> [ elem ])
|
|
||||||
|> Maybe.withDefault []
|
|
||||||
|
|
||||||
result =
|
|
||||||
"Query.first"
|
|
||||||
|> withHtmlContext (getHtmlContext elements)
|
|
||||||
in
|
|
||||||
if List.length elements == 1 then
|
|
||||||
recurse elements rest result
|
|
||||||
else
|
|
||||||
bailOut result
|
|
||||||
|
|
||||||
Index index ->
|
|
||||||
let
|
|
||||||
elements =
|
|
||||||
elmHtmlList
|
|
||||||
|> getElementAt index
|
|
||||||
|
|
||||||
result =
|
|
||||||
("Query.index " ++ toString index)
|
|
||||||
|> withHtmlContext (getHtmlContext elements)
|
|
||||||
in
|
|
||||||
if List.length elements == 1 then
|
|
||||||
recurse elements rest result
|
|
||||||
else
|
|
||||||
bailOut result
|
|
||||||
|
|
||||||
|
|
||||||
withHtmlContext : String -> String -> String
|
|
||||||
withHtmlContext htmlStr str =
|
|
||||||
String.join "\n\n" [ str, htmlStr ]
|
|
||||||
|
|
||||||
|
|
||||||
getHtmlContext : List (ElmHtml msg) -> String
|
|
||||||
getHtmlContext elmHtmlList =
|
|
||||||
if List.isEmpty elmHtmlList then
|
|
||||||
"0 matches found for this query."
|
|
||||||
else
|
|
||||||
let
|
|
||||||
maxDigits =
|
|
||||||
elmHtmlList
|
|
||||||
|> List.length
|
|
||||||
|> toString
|
|
||||||
|> String.length
|
|
||||||
in
|
|
||||||
elmHtmlList
|
|
||||||
|> List.indexedMap (printIndented maxDigits)
|
|
||||||
|> String.join "\n\n"
|
|
||||||
|
|
||||||
|
|
||||||
joinAsList : (a -> String) -> List a -> String
|
|
||||||
joinAsList toStr list =
|
|
||||||
if List.isEmpty list then
|
|
||||||
"[]"
|
|
||||||
else
|
|
||||||
"[ " ++ String.join ", " (List.map toStr list) ++ " ]"
|
|
||||||
|
|
||||||
|
|
||||||
printIndented : Int -> Int -> ElmHtml msg -> String
|
|
||||||
printIndented maxDigits index elmHtml =
|
|
||||||
let
|
|
||||||
caption =
|
|
||||||
(toString (index + 1) ++ ")")
|
|
||||||
|> String.padRight (maxDigits + 3) ' '
|
|
||||||
|> String.append baseIndentation
|
|
||||||
|
|
||||||
indentation =
|
|
||||||
String.repeat (String.length caption) " "
|
|
||||||
in
|
|
||||||
case String.split "\n" (prettyPrint elmHtml) of
|
|
||||||
[] ->
|
|
||||||
""
|
|
||||||
|
|
||||||
first :: rest ->
|
|
||||||
rest
|
|
||||||
|> List.map (String.append indentation)
|
|
||||||
|> (::) (caption ++ first)
|
|
||||||
|> String.join "\n"
|
|
||||||
|
|
||||||
|
|
||||||
baseIndentation : String
|
|
||||||
baseIndentation =
|
|
||||||
" "
|
|
||||||
|
|
||||||
|
|
||||||
prependSelector : Query msg -> SelectorQuery -> Query msg
|
|
||||||
prependSelector (Query node selectors) selector =
|
|
||||||
Query node (selector :: selectors)
|
|
||||||
|
|
||||||
|
|
||||||
{-| This is a more efficient implementation of the following:
|
|
||||||
|
|
||||||
list
|
|
||||||
|> Array.fromList
|
|
||||||
|> Array.get index
|
|
||||||
|> Maybe.map (\elem -> [ elem ])
|
|
||||||
|> Maybe.withDefault []
|
|
||||||
|
|
||||||
It also supports wraparound via negative indeces, e.g. passing -1 for an index
|
|
||||||
gets you the last element.
|
|
||||||
|
|
||||||
-}
|
|
||||||
getElementAt : Int -> List a -> List a
|
|
||||||
getElementAt index list =
|
|
||||||
let
|
|
||||||
length =
|
|
||||||
List.length list
|
|
||||||
in
|
|
||||||
-- Avoid attempting % 0
|
|
||||||
if length == 0 then
|
|
||||||
[]
|
|
||||||
else
|
|
||||||
-- Support wraparound, e.g. passing -1 to get the last element.
|
|
||||||
getElementAtHelp (index % length) list
|
|
||||||
|
|
||||||
|
|
||||||
getElementAtHelp : Int -> List a -> List a
|
|
||||||
getElementAtHelp index list =
|
|
||||||
case list of
|
|
||||||
[] ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
first :: rest ->
|
|
||||||
if index == 0 then
|
|
||||||
[ first ]
|
|
||||||
else
|
|
||||||
getElementAtHelp (index - 1) rest
|
|
||||||
|
|
||||||
|
|
||||||
traverse : Query msg -> Result QueryError (List (ElmHtml msg))
|
|
||||||
traverse (Query node selectorQueries) =
|
|
||||||
traverseSelectors selectorQueries [ Inert.toElmHtml node ]
|
|
||||||
|
|
||||||
|
|
||||||
traverseSelectors : List SelectorQuery -> List (ElmHtml msg) -> Result QueryError (List (ElmHtml msg))
|
|
||||||
traverseSelectors selectorQueries elmHtmlList =
|
|
||||||
List.foldr
|
|
||||||
(traverseSelector >> Result.andThen)
|
|
||||||
(Ok elmHtmlList)
|
|
||||||
selectorQueries
|
|
||||||
|
|
||||||
|
|
||||||
traverseSelector : SelectorQuery -> List (ElmHtml msg) -> Result QueryError (List (ElmHtml msg))
|
|
||||||
traverseSelector selectorQuery elmHtmlList =
|
|
||||||
case selectorQuery of
|
|
||||||
Find selectors ->
|
|
||||||
elmHtmlList
|
|
||||||
|> List.concatMap getChildren
|
|
||||||
|> InternalSelector.queryAll selectors
|
|
||||||
|> verifySingle "Query.find"
|
|
||||||
|> Result.map (\elem -> [ elem ])
|
|
||||||
|
|
||||||
FindAll selectors ->
|
|
||||||
elmHtmlList
|
|
||||||
|> List.concatMap getChildren
|
|
||||||
|> InternalSelector.queryAll selectors
|
|
||||||
|> Ok
|
|
||||||
|
|
||||||
Children selectors ->
|
|
||||||
elmHtmlList
|
|
||||||
|> List.concatMap getChildren
|
|
||||||
|> InternalSelector.queryAllChildren selectors
|
|
||||||
|> Ok
|
|
||||||
|
|
||||||
First ->
|
|
||||||
elmHtmlList
|
|
||||||
|> List.head
|
|
||||||
|> Maybe.map (\elem -> Ok [ elem ])
|
|
||||||
|> Maybe.withDefault (Err (NoResultsForSingle "Query.first"))
|
|
||||||
|
|
||||||
Index index ->
|
|
||||||
let
|
|
||||||
elements =
|
|
||||||
elmHtmlList
|
|
||||||
|> getElementAt index
|
|
||||||
in
|
|
||||||
if List.length elements == 1 then
|
|
||||||
Ok elements
|
|
||||||
else
|
|
||||||
Err (NoResultsForSingle ("Query.index " ++ toString index))
|
|
||||||
|
|
||||||
|
|
||||||
getChildren : ElmHtml msg -> List (ElmHtml msg)
|
|
||||||
getChildren elmHtml =
|
|
||||||
case elmHtml of
|
|
||||||
NodeEntry { children } ->
|
|
||||||
children
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
|
|
||||||
isElement : ElmHtml msg -> Bool
|
|
||||||
isElement elmHtml =
|
|
||||||
case elmHtml of
|
|
||||||
NodeEntry _ ->
|
|
||||||
True
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
verifySingle : String -> List a -> Result QueryError a
|
|
||||||
verifySingle queryName list =
|
|
||||||
case list of
|
|
||||||
[] ->
|
|
||||||
Err (NoResultsForSingle queryName)
|
|
||||||
|
|
||||||
singleton :: [] ->
|
|
||||||
Ok singleton
|
|
||||||
|
|
||||||
multiples ->
|
|
||||||
Err (MultipleResultsForSingle queryName (List.length multiples))
|
|
||||||
|
|
||||||
|
|
||||||
expectAll : (Single msg -> Expectation) -> Query msg -> Expectation
|
|
||||||
expectAll check query =
|
|
||||||
case traverse query of
|
|
||||||
Ok list ->
|
|
||||||
expectAllHelp 0 check list
|
|
||||||
|
|
||||||
Err error ->
|
|
||||||
Expect.fail (queryErrorToString query error)
|
|
||||||
|
|
||||||
|
|
||||||
expectAllHelp : Int -> (Single msg -> Expectation) -> List (ElmHtml msg) -> Expectation
|
|
||||||
expectAllHelp successes check list =
|
|
||||||
case list of
|
|
||||||
[] ->
|
|
||||||
Expect.pass
|
|
||||||
|
|
||||||
elmHtml :: rest ->
|
|
||||||
let
|
|
||||||
expectation =
|
|
||||||
Query (Inert.fromElmHtml elmHtml) []
|
|
||||||
|> Single False
|
|
||||||
|> check
|
|
||||||
in
|
|
||||||
case Test.Runner.getFailure expectation of
|
|
||||||
Just { given, message } ->
|
|
||||||
let
|
|
||||||
prefix =
|
|
||||||
if successes > 0 then
|
|
||||||
"Element #" ++ toString (successes + 1) ++ " failed this test:"
|
|
||||||
else
|
|
||||||
"The first element failed this test:"
|
|
||||||
in
|
|
||||||
[ prefix, message ]
|
|
||||||
|> String.join "\n\n"
|
|
||||||
|> Expect.fail
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
expectAllHelp (successes + 1) check rest
|
|
||||||
|
|
||||||
|
|
||||||
multipleToExpectation : Multiple msg -> (List (ElmHtml msg) -> Expectation) -> Expectation
|
|
||||||
multipleToExpectation (Multiple _ query) check =
|
|
||||||
case traverse query of
|
|
||||||
Ok list ->
|
|
||||||
check list
|
|
||||||
|
|
||||||
Err error ->
|
|
||||||
Expect.fail (queryErrorToString query error)
|
|
||||||
|
|
||||||
|
|
||||||
queryErrorToString : Query msg -> QueryError -> String
|
|
||||||
queryErrorToString query error =
|
|
||||||
case error of
|
|
||||||
NoResultsForSingle queryName ->
|
|
||||||
queryName ++ " always expects to find 1 element, but it found 0 instead."
|
|
||||||
|
|
||||||
MultipleResultsForSingle queryName resultCount ->
|
|
||||||
queryName
|
|
||||||
++ " always expects to find 1 element, but it found "
|
|
||||||
++ toString resultCount
|
|
||||||
++ " instead.\n\n\nHINT: If you actually expected "
|
|
||||||
++ toString resultCount
|
|
||||||
++ " elements, use Query.findAll instead of Query.find."
|
|
||||||
|
|
||||||
|
|
||||||
contains : List (ElmHtml msg) -> Query msg -> Expectation
|
|
||||||
contains expectedDescendants query =
|
|
||||||
case traverse query of
|
|
||||||
Ok elmHtmlList ->
|
|
||||||
let
|
|
||||||
missing =
|
|
||||||
missingDescendants elmHtmlList expectedDescendants
|
|
||||||
|
|
||||||
prettyPrint missingDescendants =
|
|
||||||
String.join
|
|
||||||
"\n\n---------------------------------------------\n\n"
|
|
||||||
(List.indexedMap
|
|
||||||
(\index descendant -> printIndented 3 index descendant)
|
|
||||||
missingDescendants
|
|
||||||
)
|
|
||||||
in
|
|
||||||
if List.isEmpty missing then
|
|
||||||
Expect.pass
|
|
||||||
else
|
|
||||||
Expect.fail
|
|
||||||
(String.join ""
|
|
||||||
[ "\t✗ /"
|
|
||||||
, toString <| List.length missing
|
|
||||||
, "\\ missing descendants: \n\n"
|
|
||||||
, prettyPrint missing
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
Err error ->
|
|
||||||
Expect.fail (queryErrorToString query error)
|
|
||||||
|
|
||||||
|
|
||||||
missingDescendants : List (ElmHtml msg) -> List (ElmHtml msg) -> List (ElmHtml msg)
|
|
||||||
missingDescendants elmHtmlList expected =
|
|
||||||
let
|
|
||||||
isMissing =
|
|
||||||
\expectedDescendant ->
|
|
||||||
not <| Descendant.isDescendant elmHtmlList expectedDescendant
|
|
||||||
in
|
|
||||||
List.filter isMissing expected
|
|
||||||
|
|
||||||
|
|
||||||
has : List Selector -> Query msg -> Expectation
|
|
||||||
has selectors query =
|
|
||||||
case traverse query of
|
|
||||||
Ok elmHtmlList ->
|
|
||||||
if List.isEmpty (InternalSelector.queryAll selectors elmHtmlList) then
|
|
||||||
selectors
|
|
||||||
|> List.map (showSelectorOutcome elmHtmlList)
|
|
||||||
|> String.join "\n"
|
|
||||||
|> Expect.fail
|
|
||||||
else
|
|
||||||
Expect.pass
|
|
||||||
|
|
||||||
Err error ->
|
|
||||||
Expect.fail (queryErrorToString query error)
|
|
||||||
|
|
||||||
|
|
||||||
hasNot : List Selector -> Query msg -> Expectation
|
|
||||||
hasNot selectors query =
|
|
||||||
case traverse query of
|
|
||||||
Ok [] ->
|
|
||||||
Expect.pass
|
|
||||||
|
|
||||||
Ok elmHtmlList ->
|
|
||||||
case InternalSelector.queryAll selectors elmHtmlList of
|
|
||||||
[] ->
|
|
||||||
Expect.pass
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
selectors
|
|
||||||
|> List.map (showSelectorOutcomeInverse elmHtmlList)
|
|
||||||
|> String.join "\n"
|
|
||||||
|> Expect.fail
|
|
||||||
|
|
||||||
Err error ->
|
|
||||||
Expect.pass
|
|
||||||
|
|
||||||
|
|
||||||
showSelectorOutcome : List (ElmHtml msg) -> Selector -> String
|
|
||||||
showSelectorOutcome elmHtmlList selector =
|
|
||||||
let
|
|
||||||
outcome =
|
|
||||||
case InternalSelector.queryAll [ selector ] elmHtmlList of
|
|
||||||
[] ->
|
|
||||||
"✗"
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
"✓"
|
|
||||||
in
|
|
||||||
String.join " " [ outcome, "has", selectorToString selector ]
|
|
||||||
|
|
||||||
|
|
||||||
showSelectorOutcomeInverse : List (ElmHtml msg) -> Selector -> String
|
|
||||||
showSelectorOutcomeInverse elmHtmlList selector =
|
|
||||||
let
|
|
||||||
outcome =
|
|
||||||
case InternalSelector.queryAll [ selector ] elmHtmlList of
|
|
||||||
[] ->
|
|
||||||
"✓"
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
"✗"
|
|
||||||
in
|
|
||||||
String.join " " [ outcome, "has not", selectorToString selector ]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- HELPERS --
|
|
||||||
|
|
||||||
|
|
||||||
failWithQuery : Bool -> String -> Query msg -> Expectation -> Expectation
|
|
||||||
failWithQuery showTrace queryName query expectation =
|
|
||||||
case Test.Runner.getFailure expectation of
|
|
||||||
Just { given, message } ->
|
|
||||||
let
|
|
||||||
lines =
|
|
||||||
toLines message query queryName
|
|
||||||
|> List.map prefixOutputLine
|
|
||||||
|
|
||||||
tracedLines =
|
|
||||||
if showTrace then
|
|
||||||
addQueryFromHtmlLine query :: lines
|
|
||||||
else
|
|
||||||
lines
|
|
||||||
in
|
|
||||||
tracedLines
|
|
||||||
|> String.join "\n\n\n"
|
|
||||||
|> Expect.fail
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
expectation
|
|
||||||
|
|
||||||
|
|
||||||
addQueryFromHtmlLine : Query msg -> String
|
|
||||||
addQueryFromHtmlLine query =
|
|
||||||
String.join "\n\n"
|
|
||||||
[ prefixOutputLine "Query.fromHtml"
|
|
||||||
, toOutputLine query
|
|
||||||
|> String.split "\n"
|
|
||||||
|> List.map ((++) baseIndentation)
|
|
||||||
|> String.join "\n"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
prefixOutputLine : String -> String
|
|
||||||
prefixOutputLine =
|
|
||||||
(++) "▼ "
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
module Test.Html.Selector
|
|
||||||
exposing
|
|
||||||
( Selector
|
|
||||||
, all
|
|
||||||
, attribute
|
|
||||||
, checked
|
|
||||||
, class
|
|
||||||
, classes
|
|
||||||
, disabled
|
|
||||||
, exactClassName
|
|
||||||
, id
|
|
||||||
, selected
|
|
||||||
, style
|
|
||||||
, tag
|
|
||||||
, text
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| Selecting HTML elements.
|
|
||||||
|
|
||||||
@docs Selector
|
|
||||||
|
|
||||||
|
|
||||||
## General Selectors
|
|
||||||
|
|
||||||
@docs tag, text, attribute, all
|
|
||||||
|
|
||||||
|
|
||||||
## Attributes
|
|
||||||
|
|
||||||
@docs id, class, classes, exactClassName, style, checked, selected, disabled
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import ElmHtml.InternalTypes
|
|
||||||
import Html exposing (Attribute)
|
|
||||||
import Html.Inert
|
|
||||||
import Json.Decode
|
|
||||||
import Test.Html.Selector.Internal as Internal exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
{-| A selector used to filter sets of elements.
|
|
||||||
-}
|
|
||||||
type alias Selector =
|
|
||||||
Internal.Selector
|
|
||||||
|
|
||||||
|
|
||||||
{-| Combine the given selectors into one which requires all of them to match.
|
|
||||||
|
|
||||||
import Html
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (class, text, all, Selector)
|
|
||||||
|
|
||||||
|
|
||||||
replyBtnSelector : Selector
|
|
||||||
replyBtnSelector =
|
|
||||||
all [ class "btn", text "Reply" ]
|
|
||||||
|
|
||||||
|
|
||||||
test "Button has the class 'btn' and the text 'Reply'" <|
|
|
||||||
\() ->
|
|
||||||
Html.button [ Attr.class "btn btn-large" ] [ Html.text "Reply" ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.has [ replyBtnSelector ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
all : List Selector -> Selector
|
|
||||||
all =
|
|
||||||
All
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have all the given classes (and possibly others as well).
|
|
||||||
|
|
||||||
When you only care about one class instead of several, you can use
|
|
||||||
[`class`](#class) instead of passing this function a list with one value in it.
|
|
||||||
|
|
||||||
To match the element's exact class attribute string, use [`className`](#className).
|
|
||||||
|
|
||||||
import Html
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "Button has the classes btn and btn-large" <|
|
|
||||||
\() ->
|
|
||||||
Html.button [ Attr.class "btn btn-large" ] [ Html.text "Reply" ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.has [ classes [ "btn", "btn-large" ] ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
classes : List String -> Selector
|
|
||||||
classes =
|
|
||||||
Classes
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have the given class (and possibly others as well).
|
|
||||||
|
|
||||||
To match multiple classes at once, use [`classes`](#classes) instead.
|
|
||||||
|
|
||||||
To match the element's exact class attribute string, use [`className`](#className).
|
|
||||||
|
|
||||||
import Html
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (class)
|
|
||||||
|
|
||||||
|
|
||||||
test "Button has the class btn-large" <|
|
|
||||||
\() ->
|
|
||||||
Html.button [ Attr.class "btn btn-large" ] [ Html.text "Reply" ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.has [ class "btn-large" ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
class : String -> Selector
|
|
||||||
class =
|
|
||||||
Class
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches the element's exact class attribute string.
|
|
||||||
|
|
||||||
This is used less often than [`class`](#class), [`classes`](#classes) or
|
|
||||||
[`attribute`](#attribute), which check for the *presence* of a class as opposed
|
|
||||||
to matching the entire class attribute exactly.
|
|
||||||
|
|
||||||
import Html
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (exactClassName)
|
|
||||||
|
|
||||||
|
|
||||||
test "Button has the exact class 'btn btn-large'" <|
|
|
||||||
\() ->
|
|
||||||
Html.button [ Attr.class "btn btn-large" ] [ Html.text "Reply" ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.has [ exactClassName "btn btn-large" ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
exactClassName : String -> Selector
|
|
||||||
exactClassName =
|
|
||||||
namedAttr "className"
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have the given `id` attribute.
|
|
||||||
|
|
||||||
import Html
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (id, text)
|
|
||||||
|
|
||||||
|
|
||||||
test "the welcome <h1> says hello!" <|
|
|
||||||
\() ->
|
|
||||||
Html.div []
|
|
||||||
[ Html.h1 [ Attr.id "welcome" ] [ Html.text "Hello!" ] ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find [ id "welcome" ]
|
|
||||||
|> Query.has [ text "Hello!" ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
id : String -> Selector
|
|
||||||
id =
|
|
||||||
namedAttr "id"
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have the given tag.
|
|
||||||
|
|
||||||
import Html
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (tag, text)
|
|
||||||
|
|
||||||
|
|
||||||
test "the welcome <h1> says hello!" <|
|
|
||||||
\() ->
|
|
||||||
Html.div []
|
|
||||||
[ Html.h1 [ Attr.id "welcome" ] [ Html.text "Hello!" ] ]
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Query.find [ tag "h1" ]
|
|
||||||
|> Query.has [ text "Hello!" ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
tag : String -> Selector
|
|
||||||
tag name =
|
|
||||||
Tag name
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have the given attribute in a way that makes sense
|
|
||||||
given their semantics in `Html`.
|
|
||||||
|
|
||||||
See [Selecting elements by `Html.Attribute msg` in the README](http://package.elm-lang.org/packages/eeue56/elm-html-test/latest#selecting-elements-by-html-attribute-msg-)
|
|
||||||
|
|
||||||
-}
|
|
||||||
attribute : Attribute Never -> Selector
|
|
||||||
attribute attr =
|
|
||||||
case Html.Inert.parseAttribute attr of
|
|
||||||
ElmHtml.InternalTypes.Attribute { key, value } ->
|
|
||||||
if String.toLower key == "class" then
|
|
||||||
value
|
|
||||||
|> String.split " "
|
|
||||||
|> Classes
|
|
||||||
else
|
|
||||||
namedAttr key value
|
|
||||||
|
|
||||||
ElmHtml.InternalTypes.Property { key, value } ->
|
|
||||||
if key == "className" then
|
|
||||||
value
|
|
||||||
|> Json.Decode.decodeValue Json.Decode.string
|
|
||||||
|> Result.map (String.split " ")
|
|
||||||
|> Result.withDefault []
|
|
||||||
|> Classes
|
|
||||||
else
|
|
||||||
value
|
|
||||||
|> Json.Decode.decodeValue Json.Decode.string
|
|
||||||
|> Result.map (namedAttr key)
|
|
||||||
|> orElseLazy
|
|
||||||
(\() ->
|
|
||||||
value
|
|
||||||
|> Json.Decode.decodeValue Json.Decode.bool
|
|
||||||
|> Result.map (namedBoolAttr key)
|
|
||||||
)
|
|
||||||
|> Result.withDefault Invalid
|
|
||||||
|
|
||||||
ElmHtml.InternalTypes.Styles styles ->
|
|
||||||
Style styles
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Invalid
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have all the given style properties (and possibly others as well).
|
|
||||||
|
|
||||||
import Html
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test exposing (test)
|
|
||||||
import Test.Html.Selector exposing (classes)
|
|
||||||
|
|
||||||
|
|
||||||
test "the Reply button has red text" <|
|
|
||||||
\() ->
|
|
||||||
Html.div []
|
|
||||||
[ Html.button [ Attr.style [ ( "color", "red" ) ] ] [ Html.text "Reply" ] ]
|
|
||||||
|> Query.has [ style [ ( "color", "red" ) ] ]
|
|
||||||
|
|
||||||
-}
|
|
||||||
style : List ( String, String ) -> Selector
|
|
||||||
style style =
|
|
||||||
Style style
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have a
|
|
||||||
[`text`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Attributes#text)
|
|
||||||
attribute with the given value.
|
|
||||||
-}
|
|
||||||
text : String -> Selector
|
|
||||||
text =
|
|
||||||
Internal.Text
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have a
|
|
||||||
[`selected`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Attributes#selected)
|
|
||||||
attribute with the given value.
|
|
||||||
-}
|
|
||||||
selected : Bool -> Selector
|
|
||||||
selected =
|
|
||||||
namedBoolAttr "selected"
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have a
|
|
||||||
[`disabled`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Attributes#disabled)
|
|
||||||
attribute with the given value.
|
|
||||||
-}
|
|
||||||
disabled : Bool -> Selector
|
|
||||||
disabled =
|
|
||||||
namedBoolAttr "disabled"
|
|
||||||
|
|
||||||
|
|
||||||
{-| Matches elements that have a
|
|
||||||
[`checked`](http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Attributes#checked)
|
|
||||||
attribute with the given value.
|
|
||||||
-}
|
|
||||||
checked : Bool -> Selector
|
|
||||||
checked =
|
|
||||||
namedBoolAttr "checked"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- HELPERS
|
|
||||||
|
|
||||||
|
|
||||||
orElseLazy : (() -> Result x a) -> Result x a -> Result x a
|
|
||||||
orElseLazy fma mb =
|
|
||||||
case mb of
|
|
||||||
Err _ ->
|
|
||||||
fma ()
|
|
||||||
|
|
||||||
Ok _ ->
|
|
||||||
mb
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
module Test.Html.Selector.Internal exposing (..)
|
|
||||||
|
|
||||||
import ElmHtml.InternalTypes exposing (ElmHtml)
|
|
||||||
import ElmHtml.Query
|
|
||||||
|
|
||||||
|
|
||||||
type Selector
|
|
||||||
= All (List Selector)
|
|
||||||
| Classes (List String)
|
|
||||||
| Class String
|
|
||||||
| Attribute { name : String, value : String }
|
|
||||||
| BoolAttribute { name : String, value : Bool }
|
|
||||||
| Style (List ( String, String ))
|
|
||||||
| Tag String
|
|
||||||
| Text String
|
|
||||||
| Invalid
|
|
||||||
|
|
||||||
|
|
||||||
selectorToString : Selector -> String
|
|
||||||
selectorToString criteria =
|
|
||||||
case criteria of
|
|
||||||
All list ->
|
|
||||||
list
|
|
||||||
|> List.map selectorToString
|
|
||||||
|> String.join " "
|
|
||||||
|
|
||||||
Classes list ->
|
|
||||||
"classes " ++ toString (String.join " " list)
|
|
||||||
|
|
||||||
Class class ->
|
|
||||||
"class " ++ toString class
|
|
||||||
|
|
||||||
Attribute { name, value } ->
|
|
||||||
"attribute "
|
|
||||||
++ toString name
|
|
||||||
++ " "
|
|
||||||
++ toString value
|
|
||||||
|
|
||||||
BoolAttribute { name, value } ->
|
|
||||||
"attribute "
|
|
||||||
++ toString name
|
|
||||||
++ " "
|
|
||||||
++ toString value
|
|
||||||
|
|
||||||
Style style ->
|
|
||||||
"styles " ++ styleToString style
|
|
||||||
|
|
||||||
Tag name ->
|
|
||||||
"tag " ++ toString name
|
|
||||||
|
|
||||||
Text text ->
|
|
||||||
"text " ++ toString text
|
|
||||||
|
|
||||||
Invalid ->
|
|
||||||
"invalid"
|
|
||||||
|
|
||||||
|
|
||||||
styleToString : List ( String, String ) -> String
|
|
||||||
styleToString style =
|
|
||||||
style
|
|
||||||
|> List.map (\( k, v ) -> k ++ ":" ++ v ++ ";")
|
|
||||||
|> String.join " "
|
|
||||||
|
|
||||||
|
|
||||||
queryAll : List Selector -> List (ElmHtml msg) -> List (ElmHtml msg)
|
|
||||||
queryAll selectors list =
|
|
||||||
case selectors of
|
|
||||||
[] ->
|
|
||||||
list
|
|
||||||
|
|
||||||
selector :: rest ->
|
|
||||||
query ElmHtml.Query.query queryAll selector list
|
|
||||||
|> queryAllChildren rest
|
|
||||||
|
|
||||||
|
|
||||||
queryAllChildren : List Selector -> List (ElmHtml msg) -> List (ElmHtml msg)
|
|
||||||
queryAllChildren selectors list =
|
|
||||||
case selectors of
|
|
||||||
[] ->
|
|
||||||
list
|
|
||||||
|
|
||||||
selector :: rest ->
|
|
||||||
query ElmHtml.Query.queryChildren queryAllChildren selector list
|
|
||||||
|> queryAllChildren rest
|
|
||||||
|
|
||||||
|
|
||||||
query :
|
|
||||||
(ElmHtml.Query.Selector -> ElmHtml msg -> List (ElmHtml msg))
|
|
||||||
-> (List Selector -> List (ElmHtml msg) -> List (ElmHtml msg))
|
|
||||||
-> Selector
|
|
||||||
-> List (ElmHtml msg)
|
|
||||||
-> List (ElmHtml msg)
|
|
||||||
query fn fnAll selector list =
|
|
||||||
case selector of
|
|
||||||
All selectors ->
|
|
||||||
fnAll selectors list
|
|
||||||
|
|
||||||
Classes classes ->
|
|
||||||
List.concatMap (fn (ElmHtml.Query.ClassList classes)) list
|
|
||||||
|
|
||||||
Class class ->
|
|
||||||
List.concatMap (fn (ElmHtml.Query.ClassList [ class ])) list
|
|
||||||
|
|
||||||
Attribute { name, value } ->
|
|
||||||
List.concatMap (fn (ElmHtml.Query.Attribute name value)) list
|
|
||||||
|
|
||||||
BoolAttribute { name, value } ->
|
|
||||||
List.concatMap (fn (ElmHtml.Query.BoolAttribute name value)) list
|
|
||||||
|
|
||||||
Style style ->
|
|
||||||
List.concatMap (fn (ElmHtml.Query.Style style)) list
|
|
||||||
|
|
||||||
Tag name ->
|
|
||||||
List.concatMap (fn (ElmHtml.Query.Tag name)) list
|
|
||||||
|
|
||||||
Text text ->
|
|
||||||
List.concatMap (fn (ElmHtml.Query.ContainsText text)) list
|
|
||||||
|
|
||||||
Invalid ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
|
|
||||||
namedAttr : String -> String -> Selector
|
|
||||||
namedAttr name value =
|
|
||||||
Attribute
|
|
||||||
{ name = name
|
|
||||||
, value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
namedBoolAttr : String -> Bool -> Selector
|
|
||||||
namedBoolAttr name value =
|
|
||||||
BoolAttribute
|
|
||||||
{ name = name
|
|
||||||
, value = value
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
module Attributes exposing (..)
|
|
||||||
|
|
||||||
import Expect
|
|
||||||
import Html.Attributes as Attr
|
|
||||||
import Json.Encode as Encode
|
|
||||||
import Test exposing (..)
|
|
||||||
import Test.Html.Selector as Selector
|
|
||||||
import Test.Html.Selector.Internal exposing (Selector(..), namedAttr, namedBoolAttr)
|
|
||||||
|
|
||||||
|
|
||||||
all : Test
|
|
||||||
all =
|
|
||||||
describe "Selector.attribute"
|
|
||||||
[ test "can generate a StringAttribute selector" <|
|
|
||||||
\() ->
|
|
||||||
Attr.title "test"
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.equal (namedAttr "title" "test")
|
|
||||||
, test "works for things like `value` which are technically properties" <|
|
|
||||||
\() ->
|
|
||||||
Attr.value "test"
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.equal (namedAttr "value" "test")
|
|
||||||
, test "can generate a BoolAttribute selector" <|
|
|
||||||
\() ->
|
|
||||||
Attr.checked True
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.equal (namedBoolAttr "checked" True)
|
|
||||||
, test "can generate a Style selector" <|
|
|
||||||
\() ->
|
|
||||||
Attr.style [ ( "margin", "0" ) ]
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.equal (Style [ ( "margin", "0" ) ])
|
|
||||||
, describe "classes" <|
|
|
||||||
[ test "can generate a Classes selector" <|
|
|
||||||
\() ->
|
|
||||||
Attr.class "hello world"
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.equal (Classes [ "hello", "world" ])
|
|
||||||
, test "catches a situation where the user passes classes using Html.Attr.attribute \"class\" \"the-class\"" <|
|
|
||||||
\() ->
|
|
||||||
Attr.attribute "class" "hello world"
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.equal (Classes [ "hello", "world" ])
|
|
||||||
, test "can find a class attribute in a case insensitive manner" <|
|
|
||||||
\() ->
|
|
||||||
Attr.attribute "CLASS" "hello world"
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.equal (Classes [ "hello", "world" ])
|
|
||||||
, test "finds className property only by exact match" <|
|
|
||||||
\() ->
|
|
||||||
Attr.property "classname" (Encode.string "hello world")
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.all
|
|
||||||
[ Expect.notEqual (Classes [ "hello world" ])
|
|
||||||
, Expect.equal (Attribute { name = "classname", value = "hello world" })
|
|
||||||
]
|
|
||||||
]
|
|
||||||
, test "anything else fails" <|
|
|
||||||
\() ->
|
|
||||||
Attr.property "unknown" (Encode.int 1)
|
|
||||||
|> Selector.attribute
|
|
||||||
|> Expect.equal Invalid
|
|
||||||
]
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
module Descendant exposing (..)
|
|
||||||
|
|
||||||
import Html exposing (Html)
|
|
||||||
import Test exposing (..)
|
|
||||||
import ElmHtml.InternalTypes exposing (ElmHtml(..))
|
|
||||||
import Expect
|
|
||||||
import Html exposing (..)
|
|
||||||
import Html.Inert exposing (fromHtml, toElmHtml)
|
|
||||||
import Test.Html.Query as Query exposing (Single)
|
|
||||||
import Test.Html.Descendant exposing (isDescendant)
|
|
||||||
|
|
||||||
|
|
||||||
wrapper : Html msg -> Html msg -> Bool
|
|
||||||
wrapper html potentialDescendant =
|
|
||||||
let
|
|
||||||
elmHtml =
|
|
||||||
[ htmlToElm html ]
|
|
||||||
|
|
||||||
potentialDescendantElmHtml =
|
|
||||||
htmlToElm potentialDescendant
|
|
||||||
in
|
|
||||||
isDescendant elmHtml potentialDescendantElmHtml
|
|
||||||
|
|
||||||
|
|
||||||
all : Test
|
|
||||||
all =
|
|
||||||
describe "Contains assertion"
|
|
||||||
[ test "returns true if it contains the expected html once" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
aSingleDescendant =
|
|
||||||
someTitle "foo"
|
|
||||||
|
|
||||||
html =
|
|
||||||
div [] [ aSingleDescendant ]
|
|
||||||
in
|
|
||||||
wrapper html aSingleDescendant
|
|
||||||
|> Expect.true ""
|
|
||||||
, test "returns true if it contains the expected html more than once" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
aMultiInstanceDescendant =
|
|
||||||
someTitle "foo"
|
|
||||||
|
|
||||||
html =
|
|
||||||
div []
|
|
||||||
[ aMultiInstanceDescendant
|
|
||||||
, aMultiInstanceDescendant
|
|
||||||
]
|
|
||||||
in
|
|
||||||
wrapper html aMultiInstanceDescendant
|
|
||||||
|> Expect.true ""
|
|
||||||
, test "return true if the node is a nested descendant" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
aNestedDescendant =
|
|
||||||
someTitle "foo"
|
|
||||||
|
|
||||||
html =
|
|
||||||
div []
|
|
||||||
[ div []
|
|
||||||
[ div [] [ aNestedDescendant ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
in
|
|
||||||
wrapper html aNestedDescendant
|
|
||||||
|> Expect.true ""
|
|
||||||
, test "returns false if it does not contain the node" <|
|
|
||||||
\() ->
|
|
||||||
let
|
|
||||||
notInHtml =
|
|
||||||
img [] []
|
|
||||||
|
|
||||||
html =
|
|
||||||
div [] [ someTitle "foo" ]
|
|
||||||
in
|
|
||||||
wrapper html notInHtml
|
|
||||||
|> Expect.false ""
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
someTitle : String -> Html msg
|
|
||||||
someTitle str =
|
|
||||||
h1 [] [ text str ]
|
|
||||||
|
|
||||||
|
|
||||||
htmlToElm : Html msg -> ElmHtml msg
|
|
||||||
htmlToElm =
|
|
||||||
toElmHtml << fromHtml
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
module Events exposing (..)
|
|
||||||
|
|
||||||
import Expect
|
|
||||||
import Html exposing (Html, button, div, input, text)
|
|
||||||
import Html.Attributes as Attr exposing (href)
|
|
||||||
import Html.Events exposing (..)
|
|
||||||
import Html.Keyed as Keyed
|
|
||||||
import Html.Lazy as Lazy
|
|
||||||
import Json.Decode exposing (Value)
|
|
||||||
import Json.Encode as Encode
|
|
||||||
import Test exposing (..)
|
|
||||||
import Test.Html.Event as Event exposing (Event)
|
|
||||||
import Test.Html.Query as Query exposing (Single)
|
|
||||||
import Test.Html.Selector exposing (tag)
|
|
||||||
|
|
||||||
|
|
||||||
all : Test
|
|
||||||
all =
|
|
||||||
describe "trigerring events"
|
|
||||||
[ test "returns msg for click on element" <|
|
|
||||||
\() ->
|
|
||||||
Query.fromHtml sampleHtml
|
|
||||||
|> Query.findAll [ tag "button" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Event.simulate Event.click
|
|
||||||
|> Event.expect SampleMsg
|
|
||||||
, test "returns msg for click on lazy html" <|
|
|
||||||
\() ->
|
|
||||||
Query.fromHtml sampleLazyHtml
|
|
||||||
|> Query.findAll [ tag "button" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Event.simulate Event.click
|
|
||||||
|> Event.expect SampleMsg
|
|
||||||
, test "returns msg for click on mapped html" <|
|
|
||||||
\() ->
|
|
||||||
Query.fromHtml sampleMappedHtml
|
|
||||||
|> Query.findAll [ tag "button" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Event.simulate Event.click
|
|
||||||
|> Event.expect MappedSampleMsg
|
|
||||||
, test "returns msg for click on mapped lazy html" <|
|
|
||||||
\() ->
|
|
||||||
Query.fromHtml sampleMappedLazyHtml
|
|
||||||
|> Query.findAll [ tag "button" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Event.simulate Event.click
|
|
||||||
|> Event.expect MappedSampleMsg
|
|
||||||
, test "returns msg for click on mapped keyed html" <|
|
|
||||||
\() ->
|
|
||||||
Query.fromHtml sampleMappedKeyedHtml
|
|
||||||
|> Query.findAll [ tag "button" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Event.simulate Event.click
|
|
||||||
|> Event.expect MappedSampleMsg
|
|
||||||
, test "returns msg for click on deep mapped html" <|
|
|
||||||
\() ->
|
|
||||||
Query.fromHtml deepMappedHtml
|
|
||||||
|> Query.findAll [ tag "input" ]
|
|
||||||
|> Query.first
|
|
||||||
|> Event.simulate (Event.input "foo")
|
|
||||||
|> Event.expect (SampleInputMsg "foobar")
|
|
||||||
, test "returns msg for input with transformation" <|
|
|
||||||
\() ->
|
|
||||||
input [ onInput (String.toUpper >> SampleInputMsg) ] []
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Event.simulate (Event.input "cats")
|
|
||||||
|> Event.expect (SampleInputMsg "CATS")
|
|
||||||
, test "returns msg for check event" <|
|
|
||||||
\() ->
|
|
||||||
input [ onCheck SampleCheckedMsg ] []
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Event.simulate (Event.check True)
|
|
||||||
|> Event.expect (SampleCheckedMsg True)
|
|
||||||
, test "returns msg for custom event" <|
|
|
||||||
\() ->
|
|
||||||
input [ on "keyup" (Json.Decode.map SampleKeyUpMsg keyCode) ] []
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Event.simulate ( "keyup", Encode.object [ ( "keyCode", Encode.int 5 ) ] )
|
|
||||||
|> Event.expect (SampleKeyUpMsg 5)
|
|
||||||
, testEvent onDoubleClick Event.doubleClick
|
|
||||||
, testEvent onMouseDown Event.mouseDown
|
|
||||||
, testEvent onMouseUp Event.mouseUp
|
|
||||||
, testEvent onMouseLeave Event.mouseLeave
|
|
||||||
, testEvent onMouseOver Event.mouseOver
|
|
||||||
, testEvent onMouseOut Event.mouseOut
|
|
||||||
, testEvent onSubmit Event.submit
|
|
||||||
, testEvent onBlur Event.blur
|
|
||||||
, testEvent onFocus Event.focus
|
|
||||||
, test "event result" <|
|
|
||||||
\() ->
|
|
||||||
Query.fromHtml sampleHtml
|
|
||||||
|> Query.find [ tag "button" ]
|
|
||||||
|> Event.simulate Event.click
|
|
||||||
|> Event.toResult
|
|
||||||
|> Expect.equal (Ok SampleMsg)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= SampleMsg
|
|
||||||
| MappedSampleMsg
|
|
||||||
| SampleInputMsg String
|
|
||||||
| SampleCheckedMsg Bool
|
|
||||||
| SampleKeyUpMsg Int
|
|
||||||
|
|
||||||
|
|
||||||
sampleHtml : Html Msg
|
|
||||||
sampleHtml =
|
|
||||||
div [ Attr.class "container" ]
|
|
||||||
[ button [ onClick SampleMsg ] [ text "click me" ]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
sampleLazyHtml : Html Msg
|
|
||||||
sampleLazyHtml =
|
|
||||||
div [ Attr.class "container" ]
|
|
||||||
[ Lazy.lazy
|
|
||||||
(\str -> button [ onClick SampleMsg ] [ text str ])
|
|
||||||
"click me"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
sampleMappedHtml : Html Msg
|
|
||||||
sampleMappedHtml =
|
|
||||||
div [ Attr.class "container" ]
|
|
||||||
[ Html.map (always MappedSampleMsg) (button [ onClick SampleMsg ] [ text "click me" ])
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
sampleMappedLazyHtml : Html Msg
|
|
||||||
sampleMappedLazyHtml =
|
|
||||||
div [ Attr.class "container" ]
|
|
||||||
[ Html.map (always MappedSampleMsg) <|
|
|
||||||
Lazy.lazy
|
|
||||||
(\str -> button [ onClick SampleMsg ] [ text str ])
|
|
||||||
"click me"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
sampleMappedKeyedHtml : Html Msg
|
|
||||||
sampleMappedKeyedHtml =
|
|
||||||
div [ Attr.class "container" ]
|
|
||||||
[ Html.map (always MappedSampleMsg) <|
|
|
||||||
Keyed.node "button"
|
|
||||||
[ onClick SampleMsg ]
|
|
||||||
[ ( "key", text "click me" ) ]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
deepMappedHtml : Html Msg
|
|
||||||
deepMappedHtml =
|
|
||||||
div []
|
|
||||||
[ Html.map SampleInputMsg
|
|
||||||
(div []
|
|
||||||
[ Html.map (\msg -> msg ++ "bar")
|
|
||||||
(div []
|
|
||||||
[ input [ onInput identity ] []
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
testEvent : (Msg -> Html.Attribute Msg) -> ( String, Value ) -> Test
|
|
||||||
testEvent testOn (( eventName, eventValue ) as event) =
|
|
||||||
test ("returns msg for " ++ eventName ++ "(" ++ toString eventValue ++ ") event") <|
|
|
||||||
\() ->
|
|
||||||
input [ testOn SampleMsg ] []
|
|
||||||
|> Query.fromHtml
|
|
||||||
|> Event.simulate event
|
|
||||||
|> Event.expect SampleMsg
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
module Queries exposing (..)
|
|
||||||
|
|
||||||
import Expect
|
|
||||||
import Html exposing (Html, a, div, footer, header, li, section, span, ul)
|
|
||||||
import Html.Attributes as Attr exposing (href)
|
|
||||||
import Html.Lazy as Lazy
|
|
||||||
import Test exposing (..)
|
|
||||||
import Test.Html.Query as Query exposing (Single)
|
|
||||||
import Test.Html.Selector exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
htmlTests : Test
|
|
||||||
htmlTests =
|
|
||||||
describe "Html" <|
|
|
||||||
List.map (\toTest -> toTest (Query.fromHtml sampleHtml)) testers
|
|
||||||
|
|
||||||
|
|
||||||
lazyTests : Test
|
|
||||||
lazyTests =
|
|
||||||
describe "lazy Html" <|
|
|
||||||
List.map (\toTest -> toTest (Query.fromHtml sampleLazyHtml)) testers
|
|
||||||
|
|
||||||
|
|
||||||
testers : List (Single msg -> Test)
|
|
||||||
testers =
|
|
||||||
[ testFindAll
|
|
||||||
, testKeep
|
|
||||||
, testFind
|
|
||||||
, testRoot
|
|
||||||
, testFirst
|
|
||||||
, testIndex
|
|
||||||
, testChildren
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
testRoot : Single msg -> Test
|
|
||||||
testRoot output =
|
|
||||||
describe "root query without find or findAll"
|
|
||||||
[ describe "finds itself" <|
|
|
||||||
[ test "sees it's a <section class='root'>" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Expect.all
|
|
||||||
[ Query.has [ class "root" ]
|
|
||||||
, Query.has [ tag "section" ]
|
|
||||||
]
|
|
||||||
, test "recognizes its exact className" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.has [ exactClassName "root" ]
|
|
||||||
, test "recognizes its class by classes" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.has [ classes [ "root" ] ]
|
|
||||||
, test "recognizes its style by a single css property" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.has [ style [ ( "color", "red" ) ] ]
|
|
||||||
, test "recognizes its style by multiple css properties" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.has [ style [ ( "color", "red" ), ( "background", "purple" ) ] ]
|
|
||||||
, test "recognizes its style does not include a css property" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.hasNot [ style [ ( "color", "green" ) ] ]
|
|
||||||
, test "recognizes if is has a specific descendant" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.contains [ someView "Such a title !" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
testFind : Single msg -> Test
|
|
||||||
testFind output =
|
|
||||||
describe "Query.find []"
|
|
||||||
[ describe "finds the one child" <|
|
|
||||||
[ test "sees it's a <div class='container'>" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find []
|
|
||||||
|> Expect.all
|
|
||||||
[ Query.has [ class "container" ]
|
|
||||||
, Query.has [ tag "div" ]
|
|
||||||
]
|
|
||||||
, test "recognizes its exact className" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find []
|
|
||||||
|> Query.has [ exactClassName "container" ]
|
|
||||||
, test "recognizes its class by classes" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find []
|
|
||||||
|> Query.has [ classes [ "container" ] ]
|
|
||||||
, test "recognizes its style by style list" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.has [ style [ ( "color", "blue" ) ] ]
|
|
||||||
, test "recognizes if is has a specific descendant" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find []
|
|
||||||
|> Query.contains [ someView "Such a title !" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
testFindAll : Single msg -> Test
|
|
||||||
testFindAll output =
|
|
||||||
describe "Query.findAll []"
|
|
||||||
[ describe "finds the one child" <|
|
|
||||||
[ test "and only the one child" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll []
|
|
||||||
|> Query.count (Expect.equal 1)
|
|
||||||
, test "sees it's a <div class='container'>" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll []
|
|
||||||
|> Expect.all
|
|
||||||
[ Query.each (Query.has [ class "container" ])
|
|
||||||
, Query.each (Query.has [ tag "div" ])
|
|
||||||
]
|
|
||||||
, test "recognizes its exact className" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll []
|
|
||||||
|> Query.each (Query.has [ exactClassName "container" ])
|
|
||||||
, test "recognizes its class by classes" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll []
|
|
||||||
|> Query.each (Query.has [ classes [ "container" ] ])
|
|
||||||
]
|
|
||||||
, describe "finds multiple descendants"
|
|
||||||
[ test "with tag selectors that return one match at the start" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "header" ]
|
|
||||||
|> Query.count (Expect.equal 1)
|
|
||||||
, test "with tag selectors that return multiple matches" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "section" ]
|
|
||||||
|> Query.count (Expect.equal 2)
|
|
||||||
, test "with tag selectors that return one match at the end" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "footer" ]
|
|
||||||
|> Query.has [ text "this is the footer" ]
|
|
||||||
, test "sees the nested div" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "div" ]
|
|
||||||
|> Query.count (Expect.equal 2)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
testKeep : Single msg -> Test
|
|
||||||
testKeep output =
|
|
||||||
describe "Query.keep"
|
|
||||||
[ test "only keep a subsect of a result" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "section" ]
|
|
||||||
|> Query.keep (tag "ul")
|
|
||||||
|> Query.keep (class "list-item")
|
|
||||||
|> Expect.all
|
|
||||||
[ Query.each (Query.has [ tag "li" ])
|
|
||||||
, Query.first >> Query.has [ text "first item" ]
|
|
||||||
]
|
|
||||||
, test "keep from the second section as well" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "section" ]
|
|
||||||
|> Query.keep (class "nested-div")
|
|
||||||
|> Query.first
|
|
||||||
|> Query.has [ text "boring section" ]
|
|
||||||
, test "keep elements from both matches" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "section" ]
|
|
||||||
|> Query.keep (class "tooltip-questions")
|
|
||||||
|> Query.count (Expect.equal 2)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
testFirst : Single msg -> Test
|
|
||||||
testFirst output =
|
|
||||||
describe "Query.first"
|
|
||||||
[ describe "finds the one child" <|
|
|
||||||
[ test "sees it's a <div class='container'>" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll []
|
|
||||||
|> Query.first
|
|
||||||
|> Query.has [ tag "div", class "container" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
testIndex : Single msg -> Test
|
|
||||||
testIndex output =
|
|
||||||
describe "Query.index"
|
|
||||||
[ describe "Query.index 0" <|
|
|
||||||
[ test "sees it's a <div class='container'>" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll []
|
|
||||||
|> Query.index 0
|
|
||||||
|> Query.has [ tag "div", class "container" ]
|
|
||||||
]
|
|
||||||
, describe "Query.index -1" <|
|
|
||||||
[ test "sees it's a <div class='container'>" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll []
|
|
||||||
|> Query.index -1
|
|
||||||
|> Query.has [ tag "div", class "container" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
testChildren : Single msg -> Test
|
|
||||||
testChildren output =
|
|
||||||
describe "Query.children"
|
|
||||||
[ describe "on the root" <|
|
|
||||||
[ test "sees the root has one child" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.children []
|
|
||||||
|> Expect.all
|
|
||||||
[ Query.count (Expect.equal 1)
|
|
||||||
, Query.each (Query.hasNot [ class "root" ])
|
|
||||||
]
|
|
||||||
, test "doesn't see the nested div" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.children [ class "nested-div" ]
|
|
||||||
|> Query.count (Expect.equal 0)
|
|
||||||
, test "only children which match the selector get returned" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ class "some-list" ]
|
|
||||||
|> Query.children [ class "selected" ]
|
|
||||||
|> Query.count (Expect.equal 1)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
sampleHtml : Html msg
|
|
||||||
sampleHtml =
|
|
||||||
section [ Attr.class "root", Attr.style [ ( "color", "red" ), ( "background", "purple" ), ( "font-weight", "bold" ) ] ]
|
|
||||||
[ div [ Attr.class "container", Attr.style [ ( "color", "blue" ) ] ]
|
|
||||||
[ header [ Attr.class "funky themed", Attr.id "heading" ]
|
|
||||||
[ a [ href "http://elm-lang.org" ] [ Html.text "home" ]
|
|
||||||
, a [ href "http://elm-lang.org/examples" ] [ Html.text "examples" ]
|
|
||||||
, a [ href "http://elm-lang.org/docs" ] [ Html.text "docs" ]
|
|
||||||
]
|
|
||||||
, someView "Such a title !"
|
|
||||||
, section [ Attr.class "funky themed", Attr.id "section" ]
|
|
||||||
[ ul [ Attr.class "some-list" ]
|
|
||||||
[ li [ Attr.class "list-item themed" ] [ Html.text "first item" ]
|
|
||||||
, li [ Attr.class "list-item themed" ] [ Html.text "second item" ]
|
|
||||||
, li [ Attr.class "list-item themed selected" ] [ Html.text "third item" ]
|
|
||||||
, li [ Attr.class "list-item themed" ] [ Html.text "fourth item" ]
|
|
||||||
, span [ Attr.class "tooltip-questions" ] [ Html.text "?" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
, section []
|
|
||||||
[ div [ Attr.class "nested-div" ] [ Html.text "boring section" ]
|
|
||||||
, span [ Attr.class "tooltip-questions" ] [ Html.text "?" ]
|
|
||||||
]
|
|
||||||
, footer []
|
|
||||||
[ Html.text "this is the footer"
|
|
||||||
, span [ Attr.class "tooltip-questions" ] [ Html.text "?" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
sampleLazyHtml : Html msg
|
|
||||||
sampleLazyHtml =
|
|
||||||
section [ Attr.class "root", Attr.style [ ( "color", "red" ), ( "background", "purple" ), ( "font-weight", "bold" ) ] ]
|
|
||||||
[ div [ Attr.class "container", Attr.style [ ( "color", "blue" ) ] ]
|
|
||||||
[ header [ Attr.class "funky themed", Attr.id "heading" ]
|
|
||||||
[ Lazy.lazy (\str -> a [ href "http://elm-lang.org" ] [ Html.text str ]) "home"
|
|
||||||
, Lazy.lazy (\str -> a [ href "http://elm-lang.org/examples" ] [ Html.text str ]) "examples"
|
|
||||||
, Lazy.lazy (\str -> a [ href "http://elm-lang.org/docs" ] [ Html.text str ]) "docs"
|
|
||||||
]
|
|
||||||
, someView "Such a title !"
|
|
||||||
, section [ Attr.class "funky themed", Attr.id "section" ]
|
|
||||||
[ ul [ Attr.class "some-list" ]
|
|
||||||
[ Lazy.lazy (\str -> li [ Attr.class "list-item themed" ] [ Html.text str ]) "first item"
|
|
||||||
, Lazy.lazy (\str -> li [ Attr.class "list-item themed" ] [ Html.text str ]) "second item"
|
|
||||||
, Lazy.lazy (\str -> li [ Attr.class "list-item themed selected" ] [ Html.text str ]) "third item"
|
|
||||||
, Lazy.lazy (\str -> li [ Attr.class "list-item themed" ] [ Html.text str ]) "fourth item"
|
|
||||||
, Lazy.lazy (\str -> span [ Attr.class "tooltip-questions" ] [ Html.text str ]) "?"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
, section []
|
|
||||||
[ div [ Attr.class "nested-div" ]
|
|
||||||
[ Html.text "boring section"
|
|
||||||
, Lazy.lazy (\str -> span [ Attr.class "tooltip-questions" ] [ Html.text str ]) "?"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
, footer []
|
|
||||||
[ Lazy.lazy2 (\a b -> Html.text <| a ++ b) "this is " "the footer"
|
|
||||||
, Lazy.lazy (\str -> span [ Attr.class "tooltip-questions" ] [ Html.text str ]) "?"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
someView : String -> Html msg
|
|
||||||
someView str =
|
|
||||||
Html.h1 [] [ Html.text str ]
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
module TestExample exposing (all)
|
|
||||||
|
|
||||||
import Expect
|
|
||||||
import Html.Attributes exposing (href)
|
|
||||||
import Test exposing (..)
|
|
||||||
import Test.Html.Query as Query
|
|
||||||
import Test.Html.Selector exposing (..)
|
|
||||||
import ExampleApp exposing (exampleModel, view)
|
|
||||||
|
|
||||||
|
|
||||||
all : Test
|
|
||||||
all =
|
|
||||||
let
|
|
||||||
output =
|
|
||||||
view exampleModel
|
|
||||||
|> Query.fromHtml
|
|
||||||
in
|
|
||||||
describe "view exampleModel"
|
|
||||||
[ test "expect 4x <li> somewhere on the page" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.count (Expect.equal 4)
|
|
||||||
, test "expect 4x <li> inside a <ul>" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "ul" ]
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.count (Expect.equal 4)
|
|
||||||
, test "expect header to have 3 links in it" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.findAll [ tag "a" ]
|
|
||||||
|> Query.count (Expect.equal 3)
|
|
||||||
, test "expect header to have a link to the Elm homepage" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ id "heading" ]
|
|
||||||
|> Query.has [ attribute <| href "http://elm-lang.org" ]
|
|
||||||
, test "expect footer to have footer text" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "footer" ]
|
|
||||||
|> Query.has [ tag "footer", text "this is the footer" ]
|
|
||||||
, test "expect each <li> to have classes list-item and themed" <|
|
|
||||||
\() ->
|
|
||||||
output
|
|
||||||
|> Query.find [ tag "ul" ]
|
|
||||||
|> Query.findAll [ tag "li" ]
|
|
||||||
|> Query.each (Query.has [ classes [ "list-item", "themed" ] ])
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "helpful summary of your project, less than 80 characters",
|
|
||||||
"repository": "https://github.com/eeue56/elm-html-test.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
".",
|
|
||||||
"../src",
|
|
||||||
"../examples"
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"native-modules": true,
|
|
||||||
"dependencies": {
|
|
||||||
"eeue56/elm-html-in-elm": "5.1.0 <= v < 6.0.0",
|
|
||||||
"eeue56/elm-html-query": "3.0.0 <= v < 4.0.0",
|
|
||||||
"elm-community/elm-test": "4.0.0 <= v < 5.0.0",
|
|
||||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# elm-package generated files
|
|
||||||
elm-stuff
|
|
||||||
# elm-repl generated files
|
|
||||||
repl-temp-*
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2017, Noah
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# elm-lazy-list
|
|
||||||
|
|
||||||
This package is used to represent an infinite list of values, to be computed as they are needed. It is mainly designed for use with Elm test.
|
|
||||||
|
|
||||||
|
|
||||||
singleton 5
|
|
||||||
|> cons 6
|
|
||||||
-- only evaluated here!
|
|
||||||
|> toList --> 6, 5
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "A lazy list of values in pure Elm",
|
|
||||||
"repository": "https://github.com/eeue56/elm-lazy-list.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exposed-modules": ["Lazy.List"],
|
|
||||||
"dependencies": {
|
|
||||||
"eeue56/elm-lazy": "1.0.0 <= v < 2.0.0",
|
|
||||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,748 +0,0 @@
|
|||||||
module Lazy.List exposing (..)
|
|
||||||
|
|
||||||
{-| Lazy list implementation in Elm.
|
|
||||||
|
|
||||||
# Types
|
|
||||||
@docs LazyList, LazyListView
|
|
||||||
|
|
||||||
# Constructors
|
|
||||||
@docs cons, empty, singleton
|
|
||||||
|
|
||||||
# Query operations
|
|
||||||
@docs isEmpty, head, tail, headAndTail, member, length
|
|
||||||
|
|
||||||
# Conversions
|
|
||||||
@docs toList, fromList, toArray, fromArray
|
|
||||||
|
|
||||||
# Map-reduce et al.
|
|
||||||
@docs map, zip, reduce, flatten, append, foldl, foldr
|
|
||||||
|
|
||||||
# Common operations
|
|
||||||
@docs intersperse, interleave, reverse, cycle, iterate, repeat, take, takeWhile, drop, dropWhile
|
|
||||||
|
|
||||||
# Filtering operations
|
|
||||||
@docs keepIf, dropIf, filterMap, unique
|
|
||||||
|
|
||||||
# Chaining operations
|
|
||||||
@docs andMap, andThen
|
|
||||||
|
|
||||||
# Useful math stuff
|
|
||||||
@docs numbers, sum, product
|
|
||||||
|
|
||||||
# All the maps!
|
|
||||||
@docs map2, map3, map4, map5
|
|
||||||
|
|
||||||
# All the zips!
|
|
||||||
@docs zip3, zip4, zip5
|
|
||||||
|
|
||||||
# All the Cartesian products!
|
|
||||||
**Warning:** Calling these functions on large lists and then calling `toList` can easily overflow the stack. Consider
|
|
||||||
passing the results to `take aConstantNumber`.
|
|
||||||
|
|
||||||
@docs product2, product3, product4, product5
|
|
||||||
|
|
||||||
# Infix Operators
|
|
||||||
@docs (:::), (+++)
|
|
||||||
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Array exposing (Array)
|
|
||||||
import List
|
|
||||||
import Random exposing (Generator, Seed)
|
|
||||||
import Lazy exposing (Lazy, lazy, force)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Analogous to `List` type. This is the actual implementation type for the
|
|
||||||
`LazyList` type. This type is exposed to the user if the user so wishes to
|
|
||||||
do pattern matching or understand how the list type works. It is not
|
|
||||||
recommended to work with this type directly. Try working solely with the
|
|
||||||
provided functions in the package.
|
|
||||||
-}
|
|
||||||
type LazyListView a
|
|
||||||
= Nil
|
|
||||||
| Cons a (LazyList a)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazy List type.
|
|
||||||
-}
|
|
||||||
type alias LazyList a =
|
|
||||||
Lazy (LazyListView a)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Create an empty list.
|
|
||||||
-}
|
|
||||||
empty : LazyList a
|
|
||||||
empty =
|
|
||||||
lazy <|
|
|
||||||
\() -> Nil
|
|
||||||
|
|
||||||
|
|
||||||
{-| Create a singleton list.
|
|
||||||
-}
|
|
||||||
singleton : a -> LazyList a
|
|
||||||
singleton a =
|
|
||||||
cons a empty
|
|
||||||
|
|
||||||
|
|
||||||
{-| Detect if a list is empty or not.
|
|
||||||
-}
|
|
||||||
isEmpty : LazyList a -> Bool
|
|
||||||
isEmpty list =
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
True
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
False
|
|
||||||
|
|
||||||
|
|
||||||
{-| Add a value to the front of a list.
|
|
||||||
-}
|
|
||||||
cons : a -> LazyList a -> LazyList a
|
|
||||||
cons a list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
Cons a list
|
|
||||||
|
|
||||||
|
|
||||||
{-| Get the head of a list.
|
|
||||||
-}
|
|
||||||
head : LazyList a -> Maybe a
|
|
||||||
head list =
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nothing
|
|
||||||
|
|
||||||
Cons first _ ->
|
|
||||||
Just first
|
|
||||||
|
|
||||||
|
|
||||||
{-| Get the tail of a list.
|
|
||||||
-}
|
|
||||||
tail : LazyList a -> Maybe (LazyList a)
|
|
||||||
tail list =
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nothing
|
|
||||||
|
|
||||||
Cons _ rest ->
|
|
||||||
Just rest
|
|
||||||
|
|
||||||
|
|
||||||
{-| Get the head and tail of a list.
|
|
||||||
-}
|
|
||||||
headAndTail : LazyList a -> Maybe ( a, LazyList a )
|
|
||||||
headAndTail list =
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nothing
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
Just ( first, rest )
|
|
||||||
|
|
||||||
|
|
||||||
{-| Repeat a value ad infinitum.
|
|
||||||
Be careful when you use this. The result of this is a truly infinite list.
|
|
||||||
Do not try calling `reduce` or `toList` on an infinite list as it'll never
|
|
||||||
finish computing. Make sure you then filter it down to a finite list with `head`
|
|
||||||
or `take` or something.
|
|
||||||
-}
|
|
||||||
repeat : a -> LazyList a
|
|
||||||
repeat a =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
Cons a (repeat a)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Append a list to another list.
|
|
||||||
-}
|
|
||||||
append : LazyList a -> LazyList a -> LazyList a
|
|
||||||
append list1 list2 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
force list2
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
force (first ::: rest +++ list2)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Interleave the elements of a list in another list. The two lists get
|
|
||||||
interleaved at the end.
|
|
||||||
-}
|
|
||||||
interleave : LazyList a -> LazyList a -> LazyList a
|
|
||||||
interleave list1 list2 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
force list2
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
case force list2 of
|
|
||||||
Nil ->
|
|
||||||
force list1
|
|
||||||
|
|
||||||
Cons first2 rest2 ->
|
|
||||||
force (first1 ::: first2 ::: interleave rest1 rest2)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Places the given value between all members of the given list.
|
|
||||||
-}
|
|
||||||
intersperse : a -> LazyList a -> LazyList a
|
|
||||||
intersperse a list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
case force rest of
|
|
||||||
Nil ->
|
|
||||||
force (first ::: empty)
|
|
||||||
|
|
||||||
Cons second tail ->
|
|
||||||
case force tail of
|
|
||||||
Nil ->
|
|
||||||
force (first ::: a ::: second ::: empty)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
force (first ::: a ::: second ::: a ::: intersperse a tail)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Take a list and repeat it ad infinitum. This cycles a finite list
|
|
||||||
by putting the front after the end of the list. This results in a no-op in
|
|
||||||
the case of an infinite list.
|
|
||||||
-}
|
|
||||||
cycle : LazyList a -> LazyList a
|
|
||||||
cycle list =
|
|
||||||
list
|
|
||||||
+++ (lazy <|
|
|
||||||
\() ->
|
|
||||||
force (cycle list)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Create an infinite list of applications of a function on some value.
|
|
||||||
|
|
||||||
Equivalent to:
|
|
||||||
|
|
||||||
x ::: f x ::: f (f x) ::: f (f (f x)) ::: ... -- etc...
|
|
||||||
-}
|
|
||||||
iterate : (a -> a) -> a -> LazyList a
|
|
||||||
iterate f a =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
Cons a (iterate f (f a))
|
|
||||||
|
|
||||||
|
|
||||||
{-| The infinite list of counting numbers.
|
|
||||||
|
|
||||||
i.e.:
|
|
||||||
|
|
||||||
1 ::: 2 ::: 3 ::: 4 ::: 5 ::: ... -- etc...
|
|
||||||
-}
|
|
||||||
numbers : LazyList number
|
|
||||||
numbers =
|
|
||||||
iterate ((+) 1) 1
|
|
||||||
|
|
||||||
|
|
||||||
{-| Take at most `n` many values from a list.
|
|
||||||
-}
|
|
||||||
take : Int -> LazyList a -> LazyList a
|
|
||||||
take n list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
if n <= 0 then
|
|
||||||
Nil
|
|
||||||
else
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
Cons first (take (n - 1) rest)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Take elements from a list as long as the predicate is satisfied.
|
|
||||||
-}
|
|
||||||
takeWhile : (a -> Bool) -> LazyList a -> LazyList a
|
|
||||||
takeWhile predicate list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
if predicate first then
|
|
||||||
Cons first (takeWhile predicate rest)
|
|
||||||
else
|
|
||||||
Nil
|
|
||||||
|
|
||||||
|
|
||||||
{-| Drop at most `n` many values from a list.
|
|
||||||
-}
|
|
||||||
drop : Int -> LazyList a -> LazyList a
|
|
||||||
drop n list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
if n <= 0 then
|
|
||||||
force list
|
|
||||||
else
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
force (drop (n - 1) rest)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Drop elements from a list as long as the predicate is satisfied.
|
|
||||||
-}
|
|
||||||
dropWhile : (a -> Bool) -> LazyList a -> LazyList a
|
|
||||||
dropWhile predicate list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
if predicate first then
|
|
||||||
force (dropWhile predicate rest)
|
|
||||||
else
|
|
||||||
force list
|
|
||||||
|
|
||||||
|
|
||||||
{-| Test if a value is a member of a list.
|
|
||||||
-}
|
|
||||||
member : a -> LazyList a -> Bool
|
|
||||||
member a list =
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
False
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
first == a || member a rest
|
|
||||||
|
|
||||||
|
|
||||||
{-| Get the length of a lazy list.
|
|
||||||
|
|
||||||
Warning: This will not terminate if the list is infinite.
|
|
||||||
-}
|
|
||||||
length : LazyList a -> Int
|
|
||||||
length =
|
|
||||||
reduce (\_ n -> n + 1) 0
|
|
||||||
|
|
||||||
|
|
||||||
{-| Remove all duplicates from a list and return a list of distinct elements.
|
|
||||||
-}
|
|
||||||
unique : LazyList a -> LazyList a
|
|
||||||
unique list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
if member first rest then
|
|
||||||
force (unique rest)
|
|
||||||
else
|
|
||||||
Cons first (unique rest)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Keep all elements in a list that satisfy the given predicate.
|
|
||||||
-}
|
|
||||||
keepIf : (a -> Bool) -> LazyList a -> LazyList a
|
|
||||||
keepIf predicate list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
if predicate first then
|
|
||||||
Cons first (keepIf predicate rest)
|
|
||||||
else
|
|
||||||
force (keepIf predicate rest)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Drop all elements in a list that satisfy the given predicate.
|
|
||||||
-}
|
|
||||||
dropIf : (a -> Bool) -> LazyList a -> LazyList a
|
|
||||||
dropIf predicate =
|
|
||||||
keepIf (\n -> not (predicate n))
|
|
||||||
|
|
||||||
|
|
||||||
{-| Map a function that may fail over a lazy list, keeping only
|
|
||||||
the values that were successfully transformed.
|
|
||||||
-}
|
|
||||||
filterMap : (a -> Maybe b) -> LazyList a -> LazyList b
|
|
||||||
filterMap transform list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
case transform first of
|
|
||||||
Just val ->
|
|
||||||
Cons val (filterMap transform rest)
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
force (filterMap transform rest)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Reduce a list with a given reducer and an initial value.
|
|
||||||
|
|
||||||
Example :
|
|
||||||
reduce (+) 0 (1 ::: 2 ::: 3 ::: 4 ::: empty) == 10
|
|
||||||
-}
|
|
||||||
reduce : (a -> b -> b) -> b -> LazyList a -> b
|
|
||||||
reduce reducer b list =
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
b
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
reduce reducer (reducer first b) rest
|
|
||||||
|
|
||||||
|
|
||||||
{-| Analogous to `List.foldl`. Is an alias for `reduce`.
|
|
||||||
-}
|
|
||||||
foldl : (a -> b -> b) -> b -> LazyList a -> b
|
|
||||||
foldl =
|
|
||||||
reduce
|
|
||||||
|
|
||||||
|
|
||||||
{-| Analogous to `List.foldr`.
|
|
||||||
-}
|
|
||||||
foldr : (a -> b -> b) -> b -> LazyList a -> b
|
|
||||||
foldr reducer b list =
|
|
||||||
Array.foldr reducer b (toArray list)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Get the sum of a list of numbers.
|
|
||||||
-}
|
|
||||||
sum : LazyList number -> number
|
|
||||||
sum =
|
|
||||||
reduce (+) 0
|
|
||||||
|
|
||||||
|
|
||||||
{-| Get the product of a list of numbers.
|
|
||||||
-}
|
|
||||||
product : LazyList number -> number
|
|
||||||
product =
|
|
||||||
reduce (*) 1
|
|
||||||
|
|
||||||
|
|
||||||
{-| Flatten a list of lists into a single list by appending all the inner
|
|
||||||
lists into one big list.
|
|
||||||
-}
|
|
||||||
flatten : LazyList (LazyList a) -> LazyList a
|
|
||||||
flatten list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
force (first +++ flatten rest)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Chain list producing operations. Map then flatten.
|
|
||||||
-}
|
|
||||||
andThen : (a -> LazyList b) -> LazyList a -> LazyList b
|
|
||||||
andThen f list =
|
|
||||||
map f list |> flatten
|
|
||||||
|
|
||||||
|
|
||||||
{-| Reverse a list.
|
|
||||||
-}
|
|
||||||
reverse : LazyList a -> LazyList a
|
|
||||||
reverse =
|
|
||||||
reduce cons empty
|
|
||||||
|
|
||||||
|
|
||||||
{-| Map a function to a list.
|
|
||||||
-}
|
|
||||||
map : (a -> b) -> LazyList a -> LazyList b
|
|
||||||
map f list =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
Cons (f first) (map f rest)
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map2 : (a -> b -> c) -> LazyList a -> LazyList b -> LazyList c
|
|
||||||
map2 f list1 list2 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
case force list2 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first2 rest2 ->
|
|
||||||
Cons (f first1 first2) (map2 f rest1 rest2)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Known as `mapN` in some circles. Allows you to apply `map` in cases
|
|
||||||
where then number of arguments are greater than 5.
|
|
||||||
|
|
||||||
The argument order is such that it works well with `|>` chains.
|
|
||||||
-}
|
|
||||||
andMap : LazyList a -> LazyList (a -> b) -> LazyList b
|
|
||||||
andMap listVal listFuncs =
|
|
||||||
map2 (<|) listFuncs listVal
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map3 : (a -> b -> c -> d) -> LazyList a -> LazyList b -> LazyList c -> LazyList d
|
|
||||||
map3 f list1 list2 list3 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
case force list2 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first2 rest2 ->
|
|
||||||
case force list3 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first3 rest3 ->
|
|
||||||
Cons (f first1 first2 first3) (map3 f rest1 rest2 rest3)
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map4 : (a -> b -> c -> d -> e) -> LazyList a -> LazyList b -> LazyList c -> LazyList d -> LazyList e
|
|
||||||
map4 f list1 list2 list3 list4 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
case force list2 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first2 rest2 ->
|
|
||||||
case force list3 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first3 rest3 ->
|
|
||||||
case force list4 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first4 rest4 ->
|
|
||||||
Cons (f first1 first2 first3 first4) (map4 f rest1 rest2 rest3 rest4)
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map5 : (a -> b -> c -> d -> e -> f) -> LazyList a -> LazyList b -> LazyList c -> LazyList d -> LazyList e -> LazyList f
|
|
||||||
map5 f list1 list2 list3 list4 list5 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
case force list2 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first2 rest2 ->
|
|
||||||
case force list3 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first3 rest3 ->
|
|
||||||
case force list4 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first4 rest4 ->
|
|
||||||
case force list5 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first5 rest5 ->
|
|
||||||
Cons
|
|
||||||
(f first1 first2 first3 first4 first5)
|
|
||||||
(map5 f rest1 rest2 rest3 rest4 rest5)
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
zip : LazyList a -> LazyList b -> LazyList ( a, b )
|
|
||||||
zip =
|
|
||||||
map2 (,)
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
zip3 : LazyList a -> LazyList b -> LazyList c -> LazyList ( a, b, c )
|
|
||||||
zip3 =
|
|
||||||
map3 (,,)
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
zip4 : LazyList a -> LazyList b -> LazyList c -> LazyList d -> LazyList ( a, b, c, d )
|
|
||||||
zip4 =
|
|
||||||
map4 (,,,)
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
zip5 : LazyList a -> LazyList b -> LazyList c -> LazyList d -> LazyList e -> LazyList ( a, b, c, d, e )
|
|
||||||
zip5 =
|
|
||||||
map5 (,,,,)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Create a lazy list containing all possible pairs in the given lazy lists.
|
|
||||||
-}
|
|
||||||
product2 : LazyList a -> LazyList b -> LazyList ( a, b )
|
|
||||||
product2 list1 list2 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
case force list2 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons _ _ ->
|
|
||||||
force <| map ((,) first1) list2 +++ product2 rest1 list2
|
|
||||||
|
|
||||||
|
|
||||||
{-| Create a lazy list containing all possible triples in the given lazy lists.
|
|
||||||
-}
|
|
||||||
product3 : LazyList a -> LazyList b -> LazyList c -> LazyList ( a, b, c )
|
|
||||||
product3 list1 list2 list3 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
force <| map (\( b, c ) -> ( first1, b, c )) (product2 list2 list3) +++ product3 rest1 list2 list3
|
|
||||||
|
|
||||||
|
|
||||||
{-| Create a lazy list containing all possible 4-tuples in the given lazy lists.
|
|
||||||
-}
|
|
||||||
product4 : LazyList a -> LazyList b -> LazyList c -> LazyList d -> LazyList ( a, b, c, d )
|
|
||||||
product4 list1 list2 list3 list4 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
force <| map (\( b, c, d ) -> ( first1, b, c, d )) (product3 list2 list3 list4) +++ product4 rest1 list2 list3 list4
|
|
||||||
|
|
||||||
|
|
||||||
{-| Create a lazy list containing all possible 5-tuples in the given lazy lists.
|
|
||||||
-}
|
|
||||||
product5 : LazyList a -> LazyList b -> LazyList c -> LazyList d -> LazyList e -> LazyList ( a, b, c, d, e )
|
|
||||||
product5 list1 list2 list3 list4 list5 =
|
|
||||||
lazy <|
|
|
||||||
\() ->
|
|
||||||
case force list1 of
|
|
||||||
Nil ->
|
|
||||||
Nil
|
|
||||||
|
|
||||||
Cons first1 rest1 ->
|
|
||||||
force <| map (\( b, c, d, e ) -> ( first1, b, c, d, e )) (product4 list2 list3 list4 list5) +++ product5 rest1 list2 list3 list4 list5
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a lazy list to a normal list.
|
|
||||||
-}
|
|
||||||
toList : LazyList a -> List a
|
|
||||||
toList list =
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
[]
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
first :: toList rest
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a normal list to a lazy list.
|
|
||||||
-}
|
|
||||||
fromList : List a -> LazyList a
|
|
||||||
fromList =
|
|
||||||
List.foldr cons empty
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert a lazy list to an array.
|
|
||||||
-}
|
|
||||||
toArray : LazyList a -> Array a
|
|
||||||
toArray list =
|
|
||||||
case force list of
|
|
||||||
Nil ->
|
|
||||||
Array.empty
|
|
||||||
|
|
||||||
Cons first rest ->
|
|
||||||
Array.append (Array.push first Array.empty) (toArray rest)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Convert an array to a lazy list.
|
|
||||||
-}
|
|
||||||
fromArray : Array a -> LazyList a
|
|
||||||
fromArray =
|
|
||||||
Array.foldr cons empty
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---------------------
|
|
||||||
-- INFIX OPERATORS --
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
|
|
||||||
infixr 5 :::
|
|
||||||
|
|
||||||
|
|
||||||
{-| Alias for `cons`. Analogous to `::` for lists.
|
|
||||||
-}
|
|
||||||
(:::) : a -> LazyList a -> LazyList a
|
|
||||||
(:::) =
|
|
||||||
cons
|
|
||||||
|
|
||||||
|
|
||||||
infixr 5 +++
|
|
||||||
|
|
||||||
|
|
||||||
{-| Alias for `append`. Analogous to `++` for lists.
|
|
||||||
-}
|
|
||||||
(+++) : LazyList a -> LazyList a -> LazyList a
|
|
||||||
(+++) =
|
|
||||||
append
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
module Example exposing (..)
|
|
||||||
|
|
||||||
import Expect exposing (Expectation)
|
|
||||||
import Fuzz exposing (Fuzzer, list, int, string)
|
|
||||||
import Test exposing (..)
|
|
||||||
import Lazy.List exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
suite : Test
|
|
||||||
suite =
|
|
||||||
describe "List.lazy"
|
|
||||||
[ test "Empty things are empty"
|
|
||||||
(\_ ->
|
|
||||||
empty
|
|
||||||
|> isEmpty
|
|
||||||
|> Expect.true "Empty is empty"
|
|
||||||
)
|
|
||||||
, test "A list of two things has two things"
|
|
||||||
(\_ ->
|
|
||||||
singleton 5
|
|
||||||
|> cons 6
|
|
||||||
|> toList
|
|
||||||
|> Expect.equal [ 6, 5 ]
|
|
||||||
)
|
|
||||||
, test "The sum is correctly computed"
|
|
||||||
(\_ ->
|
|
||||||
singleton 10
|
|
||||||
|> cons 6
|
|
||||||
|> sum
|
|
||||||
|> Expect.equal 16
|
|
||||||
)
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "Test Suites",
|
|
||||||
"repository": "https://github.com/user/project.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"../src",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"dependencies": {
|
|
||||||
"eeue56/elm-html-test": "4.1.0 <= v < 5.0.0",
|
|
||||||
"eeue56/elm-lazy": "1.0.0 <= v < 2.0.0",
|
|
||||||
"elm-community/elm-test": "4.0.0 <= v < 5.0.0",
|
|
||||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# elm-package generated files
|
|
||||||
elm-stuff
|
|
||||||
# elm-repl generated files
|
|
||||||
repl-temp-*
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2017, Noah
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# elm-lazy
|
|
||||||
|
|
||||||
|
|
||||||
This library provides a way of putting of computations until they are needed, allowing for expensive calculations later.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
lazy (\() -> sum <| List.range 1 1000000)
|
|
||||||
|
|
||||||
It also gives you a way of storing a computed value so that you do not need to re-compute it.
|
|
||||||
|
|
||||||
lazySum : Int -> Lazy Int
|
|
||||||
lazySum n =
|
|
||||||
lazy (\() -> sum <| List.range 0 n)
|
|
||||||
|
|
||||||
|
|
||||||
lazySums : List Int -> List (Lazy Int)
|
|
||||||
lazySums sums =
|
|
||||||
List.map lazySum sums
|
|
||||||
|
|
||||||
-- evaluates the head, before putting it back on the list
|
|
||||||
evaluteCurrentSum : List (Lazy Int) -> List (Lazy Int)
|
|
||||||
evaluteCurrentSum xs =
|
|
||||||
case xs of
|
|
||||||
head::rest -> Lazy.evaluate head :: rest
|
|
||||||
_ -> []
|
|
||||||
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
|
|
||||||
This is a library based originally on the old `Lazy` implementation. However, it is written entirely in pure Elm. The main difference is explicit memoization, as we no longer use side-effects to achieve laziness.
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
module CoreLazy
|
|
||||||
exposing
|
|
||||||
( Lazy
|
|
||||||
, force
|
|
||||||
, lazy
|
|
||||||
, map
|
|
||||||
, map2
|
|
||||||
, map3
|
|
||||||
, map4
|
|
||||||
, map5
|
|
||||||
, apply
|
|
||||||
, andThen
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| This library lets you delay a computation until later.
|
|
||||||
|
|
||||||
# Basics
|
|
||||||
@docs Lazy, lazy, force
|
|
||||||
|
|
||||||
# Mapping
|
|
||||||
@docs map, map2, map3, map4, map5
|
|
||||||
|
|
||||||
# Chaining
|
|
||||||
@docs apply, andThen
|
|
||||||
-}
|
|
||||||
|
|
||||||
import Native.Lazy
|
|
||||||
|
|
||||||
|
|
||||||
-- PRIMITIVES
|
|
||||||
|
|
||||||
|
|
||||||
{-| A wrapper around a value that will be lazily evaluated.
|
|
||||||
-}
|
|
||||||
type Lazy a
|
|
||||||
= Lazy (() -> a)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Delay the evaluation of a value until later. For example, maybe we will
|
|
||||||
need to generate a very long list and find its sum, but we do not want to do
|
|
||||||
it unless it is absolutely necessary.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
lazy (\() -> sum [1..1000000])
|
|
||||||
|
|
||||||
Now we only pay for `lazySum` if we actually need it.
|
|
||||||
-}
|
|
||||||
lazy : (() -> a) -> Lazy a
|
|
||||||
lazy thunk =
|
|
||||||
Lazy (Native.Lazy.memoize thunk)
|
|
||||||
|
|
||||||
|
|
||||||
{-| Force the evaluation of a lazy value. This means we only pay for the
|
|
||||||
computation when we need it. Here is a rather contrived example.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
lazy (\() -> List.sum [1..1000000])
|
|
||||||
|
|
||||||
sums : (Int, Int, Int)
|
|
||||||
sums =
|
|
||||||
(force lazySum, force lazySum, force lazySum)
|
|
||||||
|
|
||||||
We are forcing this computation three times. The cool thing is that the first
|
|
||||||
time you `force` a value, the result is stored. This means you pay the cost on
|
|
||||||
the first one, but all the rest are very cheap, basically just looking up a
|
|
||||||
value in memory.
|
|
||||||
-}
|
|
||||||
force : Lazy a -> a
|
|
||||||
force (Lazy thunk) =
|
|
||||||
thunk ()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- COMPOSING LAZINESS
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazily apply a function to a lazy value.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
map List.sum (lazy (\() -> [1..1000000]))
|
|
||||||
|
|
||||||
The resulting lazy value will create a big list and sum it up when it is
|
|
||||||
finally forced.
|
|
||||||
-}
|
|
||||||
map : (a -> b) -> Lazy a -> Lazy b
|
|
||||||
map f a =
|
|
||||||
lazy (\() -> f (force a))
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazily apply a function to two lazy values.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
lazy (\() -> List.sum [1..1000000])
|
|
||||||
|
|
||||||
lazySumPair : Lazy (Int, Int)
|
|
||||||
lazySumPair =
|
|
||||||
map2 (,) lazySum lazySum
|
|
||||||
|
|
||||||
-}
|
|
||||||
map2 : (a -> b -> result) -> Lazy a -> Lazy b -> Lazy result
|
|
||||||
map2 f a b =
|
|
||||||
lazy (\() -> f (force a) (force b))
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map3 : (a -> b -> c -> result) -> Lazy a -> Lazy b -> Lazy c -> Lazy result
|
|
||||||
map3 f a b c =
|
|
||||||
lazy (\() -> f (force a) (force b) (force c))
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map4 : (a -> b -> c -> d -> result) -> Lazy a -> Lazy b -> Lazy c -> Lazy d -> Lazy result
|
|
||||||
map4 f a b c d =
|
|
||||||
lazy (\() -> f (force a) (force b) (force c) (force d))
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map5 : (a -> b -> c -> d -> e -> result) -> Lazy a -> Lazy b -> Lazy c -> Lazy d -> Lazy e -> Lazy result
|
|
||||||
map5 f a b c d e =
|
|
||||||
lazy (\() -> f (force a) (force b) (force c) (force d) (force e))
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazily apply a lazy function to a lazy value. This is pretty rare on its
|
|
||||||
own, but it lets you map as high as you want.
|
|
||||||
|
|
||||||
map3 f a b == f `map` a `apply` b `apply` c
|
|
||||||
|
|
||||||
It is not the most beautiful, but it is equivalent and will let you create
|
|
||||||
`map9` quite easily if you really need it.
|
|
||||||
-}
|
|
||||||
apply : Lazy (a -> b) -> Lazy a -> Lazy b
|
|
||||||
apply f x =
|
|
||||||
lazy (\() -> (force f) (force x))
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazily chain together lazy computations, for when you have a series of
|
|
||||||
steps that all need to be performed lazily. This can be nice when you need to
|
|
||||||
pattern match on a value, for example, when appending lazy lists:
|
|
||||||
|
|
||||||
type List a = Empty | Node a (Lazy (List a))
|
|
||||||
|
|
||||||
cons : a -> Lazy (List a) -> Lazy (List a)
|
|
||||||
cons first rest =
|
|
||||||
Lazy.map (Node first) rest
|
|
||||||
|
|
||||||
append : Lazy (List a) -> Lazy (List a) -> Lazy (List a)
|
|
||||||
append lazyList1 lazyList2 =
|
|
||||||
let
|
|
||||||
appendHelp list1 =
|
|
||||||
case list1 of
|
|
||||||
Empty ->
|
|
||||||
lazyList2
|
|
||||||
|
|
||||||
Node first rest ->
|
|
||||||
cons first (append rest list2))
|
|
||||||
in
|
|
||||||
lazyList1
|
|
||||||
|> Lazy.andThen appendHelp
|
|
||||||
|
|
||||||
|
|
||||||
By using `andThen` we ensure that neither `lazyList1` or `lazyList2` are forced
|
|
||||||
before they are needed. So as written, the `append` function delays the pattern
|
|
||||||
matching until later.
|
|
||||||
-}
|
|
||||||
andThen : (a -> Lazy b) -> Lazy a -> Lazy b
|
|
||||||
andThen callback a =
|
|
||||||
lazy (\() -> force (callback (force a)))
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
module Main exposing (..)
|
|
||||||
|
|
||||||
import Benchmark exposing (..)
|
|
||||||
import Benchmark.Runner exposing (BenchmarkProgram, program)
|
|
||||||
import CoreLazy
|
|
||||||
import Lazy
|
|
||||||
|
|
||||||
|
|
||||||
basicInt : Lazy.Lazy Int
|
|
||||||
basicInt =
|
|
||||||
Lazy.lazy (\() -> 1)
|
|
||||||
|
|
||||||
|
|
||||||
basicCoreInt : CoreLazy.Lazy Int
|
|
||||||
basicCoreInt =
|
|
||||||
CoreLazy.lazy (\() -> 1)
|
|
||||||
|
|
||||||
|
|
||||||
bigThunk : () -> List Int
|
|
||||||
bigThunk _ =
|
|
||||||
List.range 0 10000
|
|
||||||
|
|
||||||
|
|
||||||
complexThing : () -> Lazy.Lazy (List Int)
|
|
||||||
complexThing _ =
|
|
||||||
Lazy.lazy (\() -> List.range 0 10000)
|
|
||||||
|
|
||||||
|
|
||||||
complexCoreThing : () -> CoreLazy.Lazy (List Int)
|
|
||||||
complexCoreThing _ =
|
|
||||||
CoreLazy.lazy (\() -> List.range 0 10000)
|
|
||||||
|
|
||||||
|
|
||||||
suite : Benchmark
|
|
||||||
suite =
|
|
||||||
describe "Lazy"
|
|
||||||
[ describe "force"
|
|
||||||
[ benchmark1 "force" Lazy.force basicInt
|
|
||||||
, Benchmark.compare "forcing a small int"
|
|
||||||
(benchmark1 "eeue56" Lazy.force basicInt)
|
|
||||||
(benchmark1 "core" CoreLazy.force basicCoreInt)
|
|
||||||
, Benchmark.compare "forcing a large list"
|
|
||||||
(benchmark "eeue56" (complexThing >> Lazy.force))
|
|
||||||
(benchmark "core" (complexCoreThing >> CoreLazy.force))
|
|
||||||
, Benchmark.compare "memoization"
|
|
||||||
(benchmark1 "eeue56"
|
|
||||||
(\thing ->
|
|
||||||
let
|
|
||||||
xs =
|
|
||||||
Lazy.lazy thing
|
|
||||||
|
|
||||||
firstPass =
|
|
||||||
Lazy.evaluate xs
|
|
||||||
|
|
||||||
secondPass =
|
|
||||||
Lazy.evaluate firstPass
|
|
||||||
in
|
|
||||||
secondPass |> Lazy.force
|
|
||||||
)
|
|
||||||
bigThunk
|
|
||||||
)
|
|
||||||
(benchmark1 "core"
|
|
||||||
(\thing ->
|
|
||||||
let
|
|
||||||
xs =
|
|
||||||
CoreLazy.lazy thing
|
|
||||||
|
|
||||||
firstPass =
|
|
||||||
CoreLazy.force xs
|
|
||||||
|
|
||||||
secondPass =
|
|
||||||
CoreLazy.force xs
|
|
||||||
in
|
|
||||||
xs |> CoreLazy.force
|
|
||||||
)
|
|
||||||
bigThunk
|
|
||||||
)
|
|
||||||
, Benchmark.compare "memoization foldl"
|
|
||||||
(benchmark1 "eeue56"
|
|
||||||
(\thing ->
|
|
||||||
List.foldl (\_ lazyThing -> Lazy.evaluate lazyThing) (Lazy.lazy thing) (List.range 0 5)
|
|
||||||
|> Lazy.force
|
|
||||||
)
|
|
||||||
bigThunk
|
|
||||||
)
|
|
||||||
(benchmark1 "core"
|
|
||||||
(\thing ->
|
|
||||||
List.foldl
|
|
||||||
(\_ lazyThing ->
|
|
||||||
let
|
|
||||||
_ =
|
|
||||||
CoreLazy.force lazyThing
|
|
||||||
in
|
|
||||||
lazyThing
|
|
||||||
)
|
|
||||||
(CoreLazy.lazy thing)
|
|
||||||
(List.range 0 5)
|
|
||||||
|> CoreLazy.force
|
|
||||||
)
|
|
||||||
bigThunk
|
|
||||||
)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
main : BenchmarkProgram
|
|
||||||
main =
|
|
||||||
program suite
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
var _user$project$Native_Lazy = function() {
|
|
||||||
|
|
||||||
function memoize(thunk)
|
|
||||||
{
|
|
||||||
var value;
|
|
||||||
var isForced = false;
|
|
||||||
return function(tuple0) {
|
|
||||||
if (!isForced) {
|
|
||||||
value = thunk(tuple0);
|
|
||||||
isForced = true;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
memoize: memoize
|
|
||||||
};
|
|
||||||
|
|
||||||
}();
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "helpful summary of your project, less than 80 characters",
|
|
||||||
"repository": "https://github.com/user/project.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
".",
|
|
||||||
"../src"
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"native-modules": true,
|
|
||||||
"dependencies": {
|
|
||||||
"BrianHicks/elm-benchmark": "1.0.2 <= v < 2.0.0",
|
|
||||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "Laziness and optional memoization in pure Elm",
|
|
||||||
"repository": "https://github.com/eeue56/elm-lazy.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"exposed-modules": ["Lazy"],
|
|
||||||
"dependencies": {
|
|
||||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
module Lazy
|
|
||||||
exposing
|
|
||||||
( Lazy
|
|
||||||
, force
|
|
||||||
, lazy
|
|
||||||
, evaluate
|
|
||||||
, map
|
|
||||||
, map2
|
|
||||||
, map3
|
|
||||||
, map4
|
|
||||||
, map5
|
|
||||||
, apply
|
|
||||||
, andThen
|
|
||||||
)
|
|
||||||
|
|
||||||
{-| This library lets you delay a computation until later.
|
|
||||||
|
|
||||||
# Basics
|
|
||||||
@docs Lazy, lazy, force, evaluate
|
|
||||||
|
|
||||||
# Mapping
|
|
||||||
@docs map, map2, map3, map4, map5
|
|
||||||
|
|
||||||
# Chaining
|
|
||||||
@docs apply, andThen
|
|
||||||
-}
|
|
||||||
|
|
||||||
-- PRIMITIVES
|
|
||||||
|
|
||||||
|
|
||||||
{-| A wrapper around a value that will be lazily evaluated.
|
|
||||||
-}
|
|
||||||
type Lazy a
|
|
||||||
= Lazy (() -> a)
|
|
||||||
| Evaluated a
|
|
||||||
|
|
||||||
|
|
||||||
{-| Delay the evaluation of a value until later. For example, maybe we will
|
|
||||||
need to generate a very long list and find its sum, but we do not want to do
|
|
||||||
it unless it is absolutely necessary.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
lazy (\() -> sum <| List.range 1 1000000 )
|
|
||||||
|
|
||||||
Now we only pay for `lazySum` if we actually need it.
|
|
||||||
-}
|
|
||||||
lazy : (() -> a) -> Lazy a
|
|
||||||
lazy thunk =
|
|
||||||
Lazy thunk
|
|
||||||
|
|
||||||
|
|
||||||
{-| Force the evaluation of a lazy value. This means we only pay for the
|
|
||||||
computation when we need it. Here is a rather contrived example.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
lazy (\() -> List.sum <| List.range 1 1000000)
|
|
||||||
|
|
||||||
sums : (Int, Int, Int)
|
|
||||||
sums =
|
|
||||||
(force lazySum, force lazySum, force lazySum)
|
|
||||||
-}
|
|
||||||
force : Lazy a -> a
|
|
||||||
force piece =
|
|
||||||
case piece of
|
|
||||||
Evaluated a ->
|
|
||||||
a
|
|
||||||
|
|
||||||
Lazy thunk ->
|
|
||||||
thunk ()
|
|
||||||
|
|
||||||
|
|
||||||
{-| Evaluate the lazy value if it has not already been evaluated. If it has,
|
|
||||||
do nothing.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
lazy (\() -> List.sum <| List.range 1 1000000)
|
|
||||||
|
|
||||||
sums : (Int, Int, Int)
|
|
||||||
sums =
|
|
||||||
let
|
|
||||||
evaledSum =
|
|
||||||
evaluate lazySum
|
|
||||||
in
|
|
||||||
(force evaledSum, force evaledSum, force evaledSum)
|
|
||||||
|
|
||||||
This is mainly useful for cases where you may want to store a lazy value as a
|
|
||||||
lazy value and pass it around. For example, in a list. Where possible, it is better to use
|
|
||||||
`force` and simply store the computed value seperately.
|
|
||||||
|
|
||||||
-}
|
|
||||||
evaluate : Lazy a -> Lazy a
|
|
||||||
evaluate piece =
|
|
||||||
case piece of
|
|
||||||
Evaluated a ->
|
|
||||||
Evaluated a
|
|
||||||
|
|
||||||
Lazy thunk ->
|
|
||||||
thunk ()
|
|
||||||
|> Evaluated
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- COMPOSING LAZINESS
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazily apply a function to a lazy value.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
map List.sum (lazy (\() -> <| List.range 1 1000000)
|
|
||||||
|
|
||||||
The resulting lazy value will create a big list and sum it up when it is
|
|
||||||
finally forced.
|
|
||||||
-}
|
|
||||||
map : (a -> b) -> Lazy a -> Lazy b
|
|
||||||
map f a =
|
|
||||||
lazy (\() -> f (force a))
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazily apply a function to two lazy values.
|
|
||||||
|
|
||||||
lazySum : Lazy Int
|
|
||||||
lazySum =
|
|
||||||
lazy (\() -> List.sum <| List.range 1 1000000)
|
|
||||||
|
|
||||||
lazySumPair : Lazy (Int, Int)
|
|
||||||
lazySumPair =
|
|
||||||
map2 (,) lazySum lazySum
|
|
||||||
|
|
||||||
-}
|
|
||||||
map2 : (a -> b -> result) -> Lazy a -> Lazy b -> Lazy result
|
|
||||||
map2 f a b =
|
|
||||||
lazy (\() -> f (force a) (force b))
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map3 : (a -> b -> c -> result) -> Lazy a -> Lazy b -> Lazy c -> Lazy result
|
|
||||||
map3 f a b c =
|
|
||||||
lazy (\() -> f (force a) (force b) (force c))
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map4 : (a -> b -> c -> d -> result) -> Lazy a -> Lazy b -> Lazy c -> Lazy d -> Lazy result
|
|
||||||
map4 f a b c d =
|
|
||||||
lazy (\() -> f (force a) (force b) (force c) (force d))
|
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
|
||||||
map5 : (a -> b -> c -> d -> e -> result) -> Lazy a -> Lazy b -> Lazy c -> Lazy d -> Lazy e -> Lazy result
|
|
||||||
map5 f a b c d e =
|
|
||||||
lazy (\() -> f (force a) (force b) (force c) (force d) (force e))
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazily apply a lazy function to a lazy value. This is pretty rare on its
|
|
||||||
own, but it lets you map as high as you want.
|
|
||||||
|
|
||||||
map3 f a b == f `map` a `apply` b `apply` c
|
|
||||||
|
|
||||||
It is not the most beautiful, but it is equivalent and will let you create
|
|
||||||
`map9` quite easily if you really need it.
|
|
||||||
-}
|
|
||||||
apply : Lazy (a -> b) -> Lazy a -> Lazy b
|
|
||||||
apply f x =
|
|
||||||
lazy (\() -> (force f) (force x))
|
|
||||||
|
|
||||||
|
|
||||||
{-| Lazily chain together lazy computations, for when you have a series of
|
|
||||||
steps that all need to be performed lazily. This can be nice when you need to
|
|
||||||
pattern match on a value, for example, when appending lazy lists:
|
|
||||||
|
|
||||||
type List a = Empty | Node a (Lazy (List a))
|
|
||||||
|
|
||||||
cons : a -> Lazy (List a) -> Lazy (List a)
|
|
||||||
cons first rest =
|
|
||||||
Lazy.map (Node first) rest
|
|
||||||
|
|
||||||
append : Lazy (List a) -> Lazy (List a) -> Lazy (List a)
|
|
||||||
append lazyList1 lazyList2 =
|
|
||||||
let
|
|
||||||
appendHelp list1 =
|
|
||||||
case list1 of
|
|
||||||
Empty ->
|
|
||||||
lazyList2
|
|
||||||
|
|
||||||
Node first rest ->
|
|
||||||
cons first (append rest list2))
|
|
||||||
in
|
|
||||||
lazyList1
|
|
||||||
|> Lazy.andThen appendHelp
|
|
||||||
|
|
||||||
|
|
||||||
By using `andThen` we ensure that neither `lazyList1` or `lazyList2` are forced
|
|
||||||
before they are needed. So as written, the `append` function delays the pattern
|
|
||||||
matching until later.
|
|
||||||
-}
|
|
||||||
andThen : (a -> Lazy b) -> Lazy a -> Lazy b
|
|
||||||
andThen callback a =
|
|
||||||
lazy (\() -> force (callback (force a)))
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
module Example exposing (..)
|
|
||||||
|
|
||||||
import Lazy
|
|
||||||
import Expect exposing (Expectation)
|
|
||||||
import Fuzz exposing (Fuzzer, list, int, string)
|
|
||||||
import Test exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
suite : Test
|
|
||||||
suite =
|
|
||||||
describe "Laziness"
|
|
||||||
[ fuzz int
|
|
||||||
"lazy and force"
|
|
||||||
(\x ->
|
|
||||||
Lazy.lazy (\() -> x)
|
|
||||||
|> Lazy.force
|
|
||||||
|> Expect.equal x
|
|
||||||
)
|
|
||||||
, fuzz int
|
|
||||||
"evaluate"
|
|
||||||
(\x ->
|
|
||||||
Lazy.lazy (\() -> x)
|
|
||||||
|> Lazy.evaluate
|
|
||||||
|> Lazy.force
|
|
||||||
|> Expect.equal x
|
|
||||||
)
|
|
||||||
, fuzz int
|
|
||||||
"map"
|
|
||||||
(\x ->
|
|
||||||
Lazy.lazy (\() -> x)
|
|
||||||
|> Lazy.map (\x -> x + 1)
|
|
||||||
|> Lazy.force
|
|
||||||
|> Expect.equal (x + 1)
|
|
||||||
)
|
|
||||||
, fuzz2 int
|
|
||||||
int
|
|
||||||
"map2"
|
|
||||||
(\x y ->
|
|
||||||
Lazy.map2 (\x y -> x + y) (Lazy.lazy (\() -> x)) (Lazy.lazy (\() -> y))
|
|
||||||
|> Lazy.force
|
|
||||||
|> Expect.equal (x + y)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0.0",
|
|
||||||
"summary": "Test Suites",
|
|
||||||
"repository": "https://github.com/user/project.git",
|
|
||||||
"license": "BSD3",
|
|
||||||
"source-directories": [
|
|
||||||
"../src",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"exposed-modules": [],
|
|
||||||
"dependencies": {
|
|
||||||
"eeue56/elm-html-test": "4.1.0 <= v < 5.0.0",
|
|
||||||
"elm-community/elm-test": "4.0.0 <= v < 5.0.0",
|
|
||||||
"elm-lang/core": "5.1.1 <= v < 6.0.0",
|
|
||||||
"elm-lang/html": "2.0.0 <= v < 3.0.0"
|
|
||||||
},
|
|
||||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# elm-package generated files
|
|
||||||
elm-stuff
|
|
||||||
# elm-repl generated files
|
|
||||||
repl-temp-*
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user