mirror of
https://github.com/dillonkearns/elm-pages-v3-beta.git
synced 2024-12-25 12:52:27 +03:00
Copy vendored elm-program-test as starting point for custom elm-pages state.
This commit is contained in:
parent
0b7e6053c9
commit
f541e5576b
49
examples/end-to-end/elm-program-test-src/MultiDict.elm
Normal file
49
examples/end-to-end/elm-program-test-src/MultiDict.elm
Normal file
@ -0,0 +1,49 @@
|
||||
module MultiDict exposing (MultiDict, empty, get, insert, keys, remove, set)
|
||||
|
||||
import Dict exposing (Dict)
|
||||
import List.Extra
|
||||
import List.Nonempty as NonEmpty
|
||||
|
||||
|
||||
type alias NonEmpty a =
|
||||
NonEmpty.Nonempty a
|
||||
|
||||
|
||||
type MultiDict k v
|
||||
= MultiDict (Dict k (NonEmpty v))
|
||||
|
||||
|
||||
empty : MultiDict k v
|
||||
empty =
|
||||
MultiDict Dict.empty
|
||||
|
||||
|
||||
insert : comparable -> v -> MultiDict comparable v -> MultiDict comparable v
|
||||
insert key value (MultiDict dict) =
|
||||
MultiDict
|
||||
(Dict.update key (Maybe.map (NonEmpty.cons value) >> Maybe.withDefault (NonEmpty.fromElement value) >> Just) dict)
|
||||
|
||||
|
||||
get : comparable -> MultiDict comparable v -> List v
|
||||
get key (MultiDict dict) =
|
||||
Dict.get key dict
|
||||
|> Maybe.map NonEmpty.toList
|
||||
|> Maybe.withDefault []
|
||||
|
||||
|
||||
keys : MultiDict k v -> List k
|
||||
keys (MultiDict dict) =
|
||||
Dict.toList dict
|
||||
|> List.concatMap (\( k, vs ) -> List.repeat (NonEmpty.length vs) k)
|
||||
|
||||
|
||||
remove : comparable -> v -> MultiDict comparable v -> MultiDict comparable v
|
||||
remove key value (MultiDict dict) =
|
||||
MultiDict
|
||||
(Dict.update key (Maybe.andThen (NonEmpty.toList >> List.Extra.remove value >> NonEmpty.fromList)) dict)
|
||||
|
||||
|
||||
set : comparable -> List v -> MultiDict comparable v -> MultiDict comparable v
|
||||
set key values (MultiDict dict) =
|
||||
MultiDict
|
||||
(Dict.update key (\_ -> NonEmpty.fromList values) dict)
|
2312
examples/end-to-end/elm-program-test-src/PagesProgramTest.elm
Normal file
2312
examples/end-to-end/elm-program-test-src/PagesProgramTest.elm
Normal file
File diff suppressed because it is too large
Load Diff
137
examples/end-to-end/elm-program-test-src/PairingHeap.elm
Normal file
137
examples/end-to-end/elm-program-test-src/PairingHeap.elm
Normal file
@ -0,0 +1,137 @@
|
||||
module PairingHeap exposing
|
||||
( PairingHeap, empty
|
||||
, insert, merge, findMin, deleteMin
|
||||
, fromList, toSortedList
|
||||
)
|
||||
|
||||
{-| This is a simple pairing heap implementation written in Elm usable as a priority queue. This code is
|
||||
based heavily on the pseudocode available at [the Wikipedia page](https://en.wikipedia.org/wiki/Pairing_heap).
|
||||
|
||||
|
||||
# Type and Constructor
|
||||
|
||||
@docs PairingHeap, empty
|
||||
|
||||
|
||||
# Operations
|
||||
|
||||
@docs insert, merge, findMin, deleteMin
|
||||
|
||||
|
||||
# Convenience functions
|
||||
|
||||
@docs fromList, toSortedList
|
||||
|
||||
-}
|
||||
|
||||
|
||||
{-| A `PairingHeap` has comparable keys and values of an arbitrary type.
|
||||
-}
|
||||
type PairingHeap comparable a
|
||||
= Empty
|
||||
| Heap comparable a (List (PairingHeap comparable a))
|
||||
|
||||
|
||||
{-| Create an empty PairingHeap.
|
||||
-}
|
||||
empty : PairingHeap comparable a
|
||||
empty =
|
||||
Empty
|
||||
|
||||
|
||||
{-| Find the minimum value in a heap returning Nothing if the heap is empty.
|
||||
Complexity: O(1)
|
||||
|
||||
findMin (fromList [ ( 10, () ), ( 3, () ), ( 8, () ) ]) == Just 3
|
||||
|
||||
-}
|
||||
findMin : PairingHeap comparable a -> Maybe ( comparable, a )
|
||||
findMin x =
|
||||
case x of
|
||||
Empty ->
|
||||
Nothing
|
||||
|
||||
Heap k v _ ->
|
||||
Just ( k, v )
|
||||
|
||||
|
||||
{-| Merges two `PairingHeap`s together into one new heap containing all of the key-value pairs from both inputs.
|
||||
Complexity: O(1)
|
||||
-}
|
||||
merge : PairingHeap comparable a -> PairingHeap comparable a -> PairingHeap comparable a
|
||||
merge heap1 heap2 =
|
||||
case ( heap1, heap2 ) of
|
||||
( Empty, _ ) ->
|
||||
heap2
|
||||
|
||||
( _, Empty ) ->
|
||||
heap1
|
||||
|
||||
( Heap k1 v1 hs1, Heap k2 v2 hs2 ) ->
|
||||
if k1 < k2 then
|
||||
Heap k1 v1 (heap2 :: hs1)
|
||||
|
||||
else
|
||||
Heap k2 v2 (heap1 :: hs2)
|
||||
|
||||
|
||||
{-| Inserts a new element into a `PairingHeap`.
|
||||
Complexity: O(1)
|
||||
-}
|
||||
insert : comparable -> a -> PairingHeap comparable a -> PairingHeap comparable a
|
||||
insert k v heap =
|
||||
merge (Heap k v []) heap
|
||||
|
||||
|
||||
{-| Removes the minimum element from a `PairingHeap` returning a new heap without that element.
|
||||
This will return an empty heap if given an empty heap as input.
|
||||
Complexity: O(log n)
|
||||
-}
|
||||
deleteMin : PairingHeap comparable a -> PairingHeap comparable a
|
||||
deleteMin heap =
|
||||
case heap of
|
||||
Empty ->
|
||||
Empty
|
||||
|
||||
Heap k v heaps ->
|
||||
mergePairs heaps
|
||||
|
||||
|
||||
{-| This is an internal function used by deleteMin.
|
||||
-}
|
||||
mergePairs : List (PairingHeap comparable a) -> PairingHeap comparable a
|
||||
mergePairs heaps =
|
||||
case heaps of
|
||||
[] ->
|
||||
Empty
|
||||
|
||||
x :: [] ->
|
||||
x
|
||||
|
||||
x :: (y :: xs) ->
|
||||
merge (merge x y) (mergePairs xs)
|
||||
|
||||
|
||||
|
||||
-- Extra convenience functions
|
||||
|
||||
|
||||
{-| This function turns a list of key-value pairs into a `PairingHeap`.
|
||||
Complexity: O(n)
|
||||
-}
|
||||
fromList : List ( comparable, a ) -> PairingHeap comparable a
|
||||
fromList =
|
||||
List.foldl (\( k, v ) -> insert k v) empty
|
||||
|
||||
|
||||
{-| This function turns a `PairingHeap` into a sorted list of key-value pairs.
|
||||
Complexity: O(n log n)
|
||||
-}
|
||||
toSortedList : PairingHeap comparable a -> List ( comparable, a )
|
||||
toSortedList heap =
|
||||
case heap of
|
||||
Empty ->
|
||||
[]
|
||||
|
||||
Heap k v _ ->
|
||||
( k, v ) :: toSortedList (deleteMin heap)
|
69
examples/end-to-end/elm-program-test-src/Parser/Extra.elm
Normal file
69
examples/end-to-end/elm-program-test-src/Parser/Extra.elm
Normal file
@ -0,0 +1,69 @@
|
||||
module Parser.Extra exposing (deadEndsToString)
|
||||
|
||||
{-| [No implementation for deadEndsToString · Issue #9 · elm/parser](https://github.com/elm/parser/issues/9)
|
||||
-}
|
||||
|
||||
import Parser exposing (DeadEnd, Problem(..))
|
||||
|
||||
|
||||
deadEndsToString : List DeadEnd -> String
|
||||
deadEndsToString deadEnds =
|
||||
String.join "\n" (List.map deadEndToString deadEnds)
|
||||
|
||||
|
||||
deadEndToString : DeadEnd -> String
|
||||
deadEndToString deadEnd =
|
||||
problemToString deadEnd.problem
|
||||
++ " at "
|
||||
++ deadEndToRowColString deadEnd
|
||||
|
||||
|
||||
problemToString : Problem -> String
|
||||
problemToString prob =
|
||||
case prob of
|
||||
Expecting s ->
|
||||
"Expecting " ++ s
|
||||
|
||||
ExpectingInt ->
|
||||
"Expecting Int"
|
||||
|
||||
ExpectingHex ->
|
||||
"Expecting Hex"
|
||||
|
||||
ExpectingOctal ->
|
||||
"Expecting Octal"
|
||||
|
||||
ExpectingBinary ->
|
||||
"Expecting Binary"
|
||||
|
||||
ExpectingFloat ->
|
||||
"Expecting Float"
|
||||
|
||||
ExpectingNumber ->
|
||||
"Expecting Number"
|
||||
|
||||
ExpectingVariable ->
|
||||
"Expecting Variable"
|
||||
|
||||
ExpectingSymbol s ->
|
||||
"Expecting Symbol " ++ s
|
||||
|
||||
ExpectingKeyword s ->
|
||||
"Expecting Keyword " ++ s
|
||||
|
||||
ExpectingEnd ->
|
||||
"Expecting End"
|
||||
|
||||
UnexpectedChar ->
|
||||
"Unexpected Char"
|
||||
|
||||
Problem s ->
|
||||
"Problem: " ++ s
|
||||
|
||||
BadRepeat ->
|
||||
"Bad Repeat"
|
||||
|
||||
|
||||
deadEndToRowColString : DeadEnd -> String
|
||||
deadEndToRowColString deadEnd =
|
||||
"row " ++ String.fromInt deadEnd.row ++ ", " ++ "col " ++ String.fromInt deadEnd.col
|
@ -0,0 +1,33 @@
|
||||
module Parser.Extra.String exposing (string)
|
||||
|
||||
import Parser exposing (..)
|
||||
|
||||
|
||||
string : Parser String
|
||||
string =
|
||||
succeed identity
|
||||
|. token "\""
|
||||
|= loop [] stringHelp
|
||||
|
||||
|
||||
stringHelp : List String -> Parser (Step (List String) String)
|
||||
stringHelp revChunks =
|
||||
oneOf
|
||||
[ succeed (\chunk -> Loop (chunk :: revChunks))
|
||||
|. token "\\"
|
||||
|= oneOf
|
||||
[ map (\_ -> "\n") (token "n")
|
||||
, map (\_ -> "\t") (token "t")
|
||||
, map (\_ -> "\u{000D}") (token "r")
|
||||
]
|
||||
, token "\""
|
||||
|> map (\_ -> Done (String.join "" (List.reverse revChunks)))
|
||||
, chompWhile isUninteresting
|
||||
|> getChompedString
|
||||
|> map (\chunk -> Loop (chunk :: revChunks))
|
||||
]
|
||||
|
||||
|
||||
isUninteresting : Char -> Bool
|
||||
isUninteresting char =
|
||||
char /= '\\' && char /= '"'
|
@ -0,0 +1,369 @@
|
||||
module ProgramTest.ComplexQuery exposing (ComplexQuery, Failure(..), FailureContext, FailureContext1(..), Highlight, Priority, check, exactlyOneOf, find, findButNot, run, simulate, succeed)
|
||||
|
||||
import Json.Encode as Json
|
||||
import ProgramTest.TestHtmlHacks as TestHtmlHacks
|
||||
import ProgramTest.TestHtmlParser as TestHtmlParser
|
||||
import Set exposing (Set)
|
||||
import Test.Html.Event
|
||||
import Test.Html.Query as Query
|
||||
import Test.Html.Selector as Selector exposing (Selector)
|
||||
import Test.Runner
|
||||
|
||||
|
||||
type ComplexQuery a
|
||||
= QueryResult State Highlight (List FailureContext1) (Result Failure a)
|
||||
|
||||
|
||||
succeed : a -> ComplexQuery a
|
||||
succeed a =
|
||||
QueryResult initState Set.empty [] (Ok a)
|
||||
|
||||
|
||||
type alias Priority =
|
||||
Int
|
||||
|
||||
|
||||
type alias State =
|
||||
{ priority : Priority
|
||||
}
|
||||
|
||||
|
||||
initState : State
|
||||
initState =
|
||||
{ priority = 0
|
||||
}
|
||||
|
||||
|
||||
type Failure
|
||||
= QueryFailed (List (Result String String))
|
||||
| SimulateFailed String
|
||||
| NoMatches String (List ( String, Priority, ( List FailureContext1, Failure ) ))
|
||||
| TooManyMatches String (List ( String, Priority, List FailureContext1 ))
|
||||
|
||||
|
||||
type alias FailureContext =
|
||||
List FailureContext1
|
||||
|
||||
|
||||
type FailureContext1
|
||||
= FindSucceeded (Maybe String) (() -> List String)
|
||||
| CheckSucceeded String (List FailureContext1)
|
||||
| Description (Result String String)
|
||||
|
||||
|
||||
type alias Highlight =
|
||||
Set String
|
||||
|
||||
|
||||
run : ComplexQuery a -> ( Highlight, Result ( List FailureContext1, Failure ) a )
|
||||
run (QueryResult _ highlight context result) =
|
||||
( highlight
|
||||
, case result of
|
||||
Ok a ->
|
||||
Ok a
|
||||
|
||||
Err error ->
|
||||
Err ( List.reverse context, error )
|
||||
)
|
||||
|
||||
|
||||
find : Maybe String -> List String -> List Selector -> ComplexQuery (Query.Single msg) -> ComplexQuery (Query.Single msg)
|
||||
find description highlight selectors prev =
|
||||
case prev of
|
||||
QueryResult _ _ _ (Err _) ->
|
||||
prev
|
||||
|
||||
QueryResult state prevHighlight prevContext (Ok source) ->
|
||||
case Test.Runner.getFailureReason (Query.has [ Selector.all selectors ] source) of
|
||||
Just _ ->
|
||||
let
|
||||
error =
|
||||
firstErrorOf source
|
||||
[ selectors
|
||||
, [ Selector.all selectors ]
|
||||
]
|
||||
|
||||
context =
|
||||
case description of
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
Just desc ->
|
||||
[ Description (Err desc) ]
|
||||
in
|
||||
QueryResult
|
||||
{ state
|
||||
| priority = state.priority + countSuccesses error
|
||||
}
|
||||
(Set.union (Set.fromList highlight) prevHighlight)
|
||||
(context ++ prevContext)
|
||||
(Err (QueryFailed error))
|
||||
|
||||
Nothing ->
|
||||
QueryResult
|
||||
{ state
|
||||
| priority = state.priority + List.length selectors
|
||||
}
|
||||
(Set.union (Set.fromList highlight) prevHighlight)
|
||||
(FindSucceeded description (\() -> TestHtmlHacks.getPassingSelectors selectors source) :: prevContext)
|
||||
(Ok (Query.find selectors source))
|
||||
|
||||
|
||||
exactlyOneOf : String -> List ( String, ComplexQuery a -> ComplexQuery b ) -> ComplexQuery a -> ComplexQuery b
|
||||
exactlyOneOf description options prev =
|
||||
case prev of
|
||||
QueryResult state prevHighlight prevContext (Err err) ->
|
||||
QueryResult state prevHighlight prevContext (Err err)
|
||||
|
||||
QueryResult state prevHighlight prevContext (Ok _) ->
|
||||
let
|
||||
results : List ( String, ComplexQuery b )
|
||||
results =
|
||||
List.map (Tuple.mapSecond (\option -> option prev)) options
|
||||
|
||||
successes : List ( String, ComplexQuery b )
|
||||
successes =
|
||||
List.filter (isSuccess << Tuple.second) results
|
||||
|
||||
isSuccess res =
|
||||
case res of
|
||||
QueryResult _ _ _ (Err _) ->
|
||||
False
|
||||
|
||||
QueryResult _ _ _ (Ok _) ->
|
||||
True
|
||||
|
||||
collectHighlight (QueryResult _ highlight _ _) =
|
||||
highlight
|
||||
|
||||
highlights =
|
||||
List.map (collectHighlight << Tuple.second) results
|
||||
|> List.foldl Set.union Set.empty
|
||||
|
||||
collectError ( desc, QueryResult newState _ context result ) =
|
||||
case result of
|
||||
Ok _ ->
|
||||
Nothing
|
||||
|
||||
Err x ->
|
||||
Just
|
||||
( desc
|
||||
, newState.priority
|
||||
, ( List.reverse context, x )
|
||||
)
|
||||
in
|
||||
case successes of
|
||||
[ ( _, one ) ] ->
|
||||
one
|
||||
|
||||
[] ->
|
||||
let
|
||||
failures =
|
||||
List.filterMap collectError results
|
||||
in
|
||||
QueryResult
|
||||
state
|
||||
(Set.union highlights prevHighlight)
|
||||
prevContext
|
||||
(Err (NoMatches description failures))
|
||||
|
||||
many ->
|
||||
let
|
||||
matches =
|
||||
List.map
|
||||
(\( desc, QueryResult newState _ context _ ) ->
|
||||
( desc, newState.priority, context )
|
||||
)
|
||||
many
|
||||
in
|
||||
QueryResult
|
||||
state
|
||||
(Set.union highlights prevHighlight)
|
||||
prevContext
|
||||
(Err (TooManyMatches description matches))
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
- `good`: the primary selector that must match
|
||||
- `bads`: a list of selectors that must NOT match
|
||||
- `onError`: the selector to use to produce an error message if any of the checks fail
|
||||
|
||||
-}
|
||||
findButNot :
|
||||
Maybe String
|
||||
-> List String
|
||||
->
|
||||
{ good : List Selector
|
||||
, bads : List (List Selector)
|
||||
, onError : List Selector
|
||||
}
|
||||
-> ComplexQuery (Query.Single msg)
|
||||
-> ComplexQuery (Query.Single msg)
|
||||
findButNot description highlight { good, bads, onError } prev =
|
||||
case prev of
|
||||
QueryResult _ _ _ (Err _) ->
|
||||
prev
|
||||
|
||||
QueryResult state prevHighlight prevContext (Ok source) ->
|
||||
-- This is tricky because Test.Html doesn't provide a way to search for an attribute being *not* present.
|
||||
-- So we have to check if a selector we don't want *is* present, and manually force a failure if it is.
|
||||
let
|
||||
addDescription =
|
||||
case description of
|
||||
Nothing ->
|
||||
[]
|
||||
|
||||
Just desc ->
|
||||
[ Description (Err desc) ]
|
||||
|
||||
checkBads : Priority -> List (List Selector) -> Query.Single msg -> ComplexQuery (Query.Single msg)
|
||||
checkBads extraPriority bads_ found =
|
||||
case bads_ of
|
||||
[] ->
|
||||
QueryResult
|
||||
{ state | priority = state.priority + extraPriority + 1 }
|
||||
(Set.union (Set.fromList highlight) prevHighlight)
|
||||
-- TODO: add the not bads to the context (or alternatively, add the "onErrors", but convert them all to successes)
|
||||
(FindSucceeded description (\() -> TestHtmlHacks.getPassingSelectors good source) :: prevContext)
|
||||
(Ok found)
|
||||
|
||||
nextBad :: rest ->
|
||||
let
|
||||
isBad =
|
||||
Query.has [ Selector.all nextBad ] source
|
||||
in
|
||||
case Test.Runner.getFailureReason isBad of
|
||||
Nothing ->
|
||||
-- the element matches the bad selectors; produce a Query using the onError selectors that will fail that will show a reasonable failure message
|
||||
let
|
||||
error =
|
||||
firstErrorOf source
|
||||
[ good
|
||||
, [ Selector.all good ]
|
||||
, onError
|
||||
, [ Selector.all onError ]
|
||||
]
|
||||
in
|
||||
QueryResult
|
||||
{ state | priority = state.priority + extraPriority + countSuccesses error }
|
||||
(Set.union (Set.fromList highlight) prevHighlight)
|
||||
(addDescription ++ prevContext)
|
||||
(Err (QueryFailed error))
|
||||
|
||||
Just _ ->
|
||||
-- the element we found is not bad; continue on to the next check
|
||||
checkBads (extraPriority + List.length nextBad) rest found
|
||||
|
||||
isGood =
|
||||
Query.has [ Selector.all good ] source
|
||||
in
|
||||
case Test.Runner.getFailureReason isGood of
|
||||
Just _ ->
|
||||
-- Couldn't find it, so report the best error message we can
|
||||
let
|
||||
error =
|
||||
firstErrorOf source
|
||||
[ good
|
||||
, [ Selector.all good ]
|
||||
]
|
||||
in
|
||||
QueryResult
|
||||
{ state | priority = state.priority + countSuccesses error }
|
||||
(Set.union (Set.fromList highlight) prevHighlight)
|
||||
prevContext
|
||||
(Err (QueryFailed error))
|
||||
|
||||
Nothing ->
|
||||
Query.find good source
|
||||
|> checkBads (List.length good) bads
|
||||
|
||||
|
||||
simulate : ( String, Json.Value ) -> ComplexQuery (Query.Single msg) -> ComplexQuery msg
|
||||
simulate event prev =
|
||||
case prev of
|
||||
QueryResult state prevHighlight prevContext (Err err) ->
|
||||
QueryResult state prevHighlight prevContext (Err err)
|
||||
|
||||
QueryResult state prevHighlight prevContext (Ok source) ->
|
||||
case
|
||||
source
|
||||
|> Test.Html.Event.simulate event
|
||||
|> Test.Html.Event.toResult
|
||||
of
|
||||
Err message ->
|
||||
QueryResult
|
||||
state
|
||||
prevHighlight
|
||||
(Description (Err ("simulate " ++ Tuple.first event)) :: prevContext)
|
||||
(Err (SimulateFailed (TestHtmlHacks.parseSimulateFailure message)))
|
||||
|
||||
Ok msg ->
|
||||
QueryResult state prevHighlight prevContext (Ok msg)
|
||||
|
||||
|
||||
{-| Ensure that the given query succeeds, but then ignore its result.
|
||||
-}
|
||||
check : String -> (ComplexQuery a -> ComplexQuery ignored) -> ComplexQuery a -> ComplexQuery a
|
||||
check description checkQuery prev =
|
||||
case prev of
|
||||
QueryResult _ _ _ (Err _) ->
|
||||
prev
|
||||
|
||||
QueryResult state prevHighlight prevContext (Ok source) ->
|
||||
let
|
||||
(QueryResult checkedState highlight checkContext checkResult) =
|
||||
checkQuery (QueryResult state prevHighlight [] (Ok source))
|
||||
in
|
||||
case checkResult of
|
||||
Err failure ->
|
||||
QueryResult
|
||||
checkedState
|
||||
(Set.union highlight prevHighlight)
|
||||
(Description (Err description) :: checkContext ++ prevContext)
|
||||
(Err failure)
|
||||
|
||||
Ok _ ->
|
||||
QueryResult
|
||||
{ state | priority = checkedState.priority }
|
||||
(Set.union highlight prevHighlight)
|
||||
(CheckSucceeded description checkContext :: prevContext)
|
||||
(Ok source)
|
||||
|
||||
|
||||
firstErrorOf : Query.Single msg -> List (List Selector) -> List (Result String String)
|
||||
firstErrorOf source choices =
|
||||
case choices of
|
||||
[] ->
|
||||
[ Err "PLEASE REPORT THIS AT <https://github.com/avh4/elm-program-test/issues>: firstErrorOf: asked to report an error but none of the choices failed" ]
|
||||
|
||||
next :: rest ->
|
||||
case Test.Runner.getFailureReason (Query.has next source) of
|
||||
Just reason ->
|
||||
case TestHtmlHacks.parseFailureReportWithoutHtml reason.description of
|
||||
Ok (TestHtmlParser.QueryFailure _ _ (TestHtmlParser.Has _ results)) ->
|
||||
results
|
||||
|
||||
Ok (TestHtmlParser.EventFailure name _) ->
|
||||
[ Err ("PLEASE REPORT THIS AT <https://github.com/avh4/elm-program-test/issues>: firstErrorOf: got unexpected EventFailure \"" ++ name ++ "\"") ]
|
||||
|
||||
Err err ->
|
||||
[ Err ("PLEASE REPORT THIS AT <https://github.com/avh4/elm-program-test/issues>: firstErrorOf: couldn't parse failure report: " ++ err) ]
|
||||
|
||||
Nothing ->
|
||||
firstErrorOf source rest
|
||||
|
||||
|
||||
countSuccesses : List (Result String String) -> Int
|
||||
countSuccesses results =
|
||||
List.length (List.filter isOk results)
|
||||
|
||||
|
||||
isOk : Result x a -> Bool
|
||||
isOk result =
|
||||
case result of
|
||||
Ok _ ->
|
||||
True
|
||||
|
||||
Err _ ->
|
||||
False
|
@ -0,0 +1,122 @@
|
||||
module ProgramTest.EffectSimulation exposing
|
||||
( EffectSimulation
|
||||
, SimulationState
|
||||
, clearOutgoingPortValues
|
||||
, emptySimulationState
|
||||
, init
|
||||
, outgoingPortValues
|
||||
, queueTask
|
||||
, stepWorkQueue
|
||||
)
|
||||
|
||||
import Dict exposing (Dict)
|
||||
import Fifo exposing (Fifo)
|
||||
import Json.Encode
|
||||
import MultiDict exposing (MultiDict)
|
||||
import PairingHeap exposing (PairingHeap)
|
||||
import SimulatedEffect exposing (SimulatedEffect, SimulatedTask)
|
||||
import Time
|
||||
|
||||
|
||||
type alias EffectSimulation msg effect =
|
||||
{ deconstructEffect :
|
||||
effect
|
||||
-> SimulatedEffect msg -- TODO: this should not be in here
|
||||
, workQueue : Fifo (SimulatedTask msg msg)
|
||||
, state : SimulationState msg
|
||||
, outgoingPortValues : Dict String (List Json.Encode.Value)
|
||||
}
|
||||
|
||||
|
||||
init : (effect -> SimulatedEffect msg) -> EffectSimulation msg effect
|
||||
init f =
|
||||
{ deconstructEffect = f
|
||||
, workQueue = Fifo.empty
|
||||
, state = emptySimulationState
|
||||
, outgoingPortValues = Dict.empty
|
||||
}
|
||||
|
||||
|
||||
type alias SimulationState msg =
|
||||
{ http : MultiDict ( String, String ) (SimulatedEffect.HttpRequest msg msg)
|
||||
, futureTasks : PairingHeap Int (() -> SimulatedTask msg msg)
|
||||
, nowMs : Int
|
||||
}
|
||||
|
||||
|
||||
emptySimulationState : SimulationState msg
|
||||
emptySimulationState =
|
||||
{ http = MultiDict.empty
|
||||
, futureTasks = PairingHeap.empty
|
||||
, nowMs = 0
|
||||
}
|
||||
|
||||
|
||||
queueTask : SimulatedTask msg msg -> EffectSimulation msg effect -> EffectSimulation msg effect
|
||||
queueTask task simulation =
|
||||
{ simulation
|
||||
| workQueue = Fifo.insert task simulation.workQueue
|
||||
}
|
||||
|
||||
|
||||
stepWorkQueue : EffectSimulation msg effect -> Maybe ( EffectSimulation msg effect, Maybe msg )
|
||||
stepWorkQueue simulation =
|
||||
case Fifo.remove simulation.workQueue of
|
||||
( Nothing, _ ) ->
|
||||
Nothing
|
||||
|
||||
( Just task, rest ) ->
|
||||
let
|
||||
( newState, msg ) =
|
||||
simulateTask task simulation.state
|
||||
in
|
||||
Just
|
||||
( { simulation
|
||||
| workQueue = rest
|
||||
, state = newState
|
||||
}
|
||||
, msg
|
||||
)
|
||||
|
||||
|
||||
simulateTask : SimulatedTask msg msg -> SimulationState msg -> ( SimulationState msg, Maybe msg )
|
||||
simulateTask task simulationState =
|
||||
case task of
|
||||
SimulatedEffect.Succeed msg ->
|
||||
( simulationState, Just msg )
|
||||
|
||||
SimulatedEffect.Fail msg ->
|
||||
( simulationState, Just msg )
|
||||
|
||||
SimulatedEffect.HttpTask request ->
|
||||
( { simulationState
|
||||
| http =
|
||||
MultiDict.insert ( request.method, request.url )
|
||||
request
|
||||
simulationState.http
|
||||
}
|
||||
, Nothing
|
||||
)
|
||||
|
||||
SimulatedEffect.SleepTask delay onResult ->
|
||||
( { simulationState
|
||||
| futureTasks =
|
||||
PairingHeap.insert (simulationState.nowMs + round delay) onResult simulationState.futureTasks
|
||||
}
|
||||
, Nothing
|
||||
)
|
||||
|
||||
SimulatedEffect.NowTask onResult ->
|
||||
simulateTask (onResult (Time.millisToPosix simulationState.nowMs)) simulationState
|
||||
|
||||
|
||||
outgoingPortValues : String -> EffectSimulation msg effect -> List Json.Encode.Value
|
||||
outgoingPortValues portName simulation =
|
||||
Dict.get portName simulation.outgoingPortValues
|
||||
|> Maybe.withDefault []
|
||||
|> List.reverse
|
||||
|
||||
|
||||
clearOutgoingPortValues : String -> EffectSimulation msg effect -> EffectSimulation msg effect
|
||||
clearOutgoingPortValues portName simulation =
|
||||
{ simulation | outgoingPortValues = Dict.remove portName simulation.outgoingPortValues }
|
360
examples/end-to-end/elm-program-test-src/ProgramTest/Failure.elm
Normal file
360
examples/end-to-end/elm-program-test-src/ProgramTest/Failure.elm
Normal file
@ -0,0 +1,360 @@
|
||||
module ProgramTest.Failure exposing (Failure(..), toString)
|
||||
|
||||
import Html exposing (Html)
|
||||
import ProgramTest.ComplexQuery as ComplexQuery exposing (Failure(..), FailureContext1(..))
|
||||
import ProgramTest.TestHtmlHacks as TestHtmlHacks
|
||||
import Set
|
||||
import String.Extra
|
||||
import Test.Html.Query as Query
|
||||
import Test.Runner.Failure
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
type Failure
|
||||
= ChangedPage String Url
|
||||
-- Errors
|
||||
| ExpectFailed String String Test.Runner.Failure.Reason
|
||||
| SimulateFailed String String
|
||||
| SimulateFailedToFindTarget String String
|
||||
| SimulateLastEffectFailed String
|
||||
| InvalidLocationUrl String String
|
||||
| InvalidFlags String String
|
||||
| ProgramDoesNotSupportNavigation String
|
||||
| NoBaseUrl String String
|
||||
| NoMatchingHttpRequest Int Int String { method : String, url : String } (List ( String, String ))
|
||||
| MultipleMatchingHttpRequest Int Int String { method : String, url : String } (List ( String, String ))
|
||||
| EffectSimulationNotConfigured String
|
||||
| ViewAssertionFailed String (Html ()) ComplexQuery.Highlight ( ComplexQuery.FailureContext, ComplexQuery.Failure )
|
||||
| CustomFailure String String
|
||||
|
||||
|
||||
toString : Failure -> String
|
||||
toString failure =
|
||||
case failure of
|
||||
ChangedPage cause finalLocation ->
|
||||
cause ++ " caused the program to end by navigating to " ++ String.Extra.escape (Url.toString finalLocation) ++ ". NOTE: If this is what you intended, use ProgramTest.expectPageChange to end your test."
|
||||
|
||||
ExpectFailed expectationName description reason ->
|
||||
expectationName ++ ":\n" ++ Test.Runner.Failure.format description reason
|
||||
|
||||
SimulateFailed functionName message ->
|
||||
functionName ++ ":\n" ++ message
|
||||
|
||||
SimulateFailedToFindTarget functionName message ->
|
||||
functionName ++ ":\n" ++ message
|
||||
|
||||
SimulateLastEffectFailed message ->
|
||||
"simulateLastEffect failed: " ++ message
|
||||
|
||||
InvalidLocationUrl functionName invalidUrl ->
|
||||
functionName ++ ": " ++ "Not a valid absolute URL:\n" ++ String.Extra.escape invalidUrl
|
||||
|
||||
InvalidFlags functionName message ->
|
||||
functionName ++ ":\n" ++ message
|
||||
|
||||
ProgramDoesNotSupportNavigation functionName ->
|
||||
functionName ++ ": Program does not support navigation. Use ProgramTest.createApplication to create a ProgramTest that supports navigation."
|
||||
|
||||
NoBaseUrl functionName relativeUrl ->
|
||||
functionName ++ ": The ProgramTest does not have a base URL and cannot resolve the relative URL " ++ String.Extra.escape relativeUrl ++ ". Use ProgramTest.withBaseUrl before calling ProgramTest.start to create a ProgramTest that can resolve relative URLs."
|
||||
|
||||
NoMatchingHttpRequest expected actual functionName request pendingRequests ->
|
||||
String.concat
|
||||
[ functionName
|
||||
, ": "
|
||||
, "Expected "
|
||||
, case expected of
|
||||
1 ->
|
||||
"HTTP request"
|
||||
|
||||
_ ->
|
||||
"at least " ++ String.fromInt expected ++ " HTTP requests"
|
||||
, " ("
|
||||
, request.method
|
||||
, " "
|
||||
, request.url
|
||||
, ") to have been made and still be pending, "
|
||||
, case actual of
|
||||
0 ->
|
||||
"but no such requests were made."
|
||||
|
||||
_ ->
|
||||
"but only " ++ String.fromInt actual ++ " such requests were made."
|
||||
, "\n"
|
||||
, case pendingRequests of
|
||||
[] ->
|
||||
" No requests were made."
|
||||
|
||||
_ ->
|
||||
String.concat
|
||||
[ " The following requests were made:\n"
|
||||
, String.join "\n" <|
|
||||
List.map (\( method, url ) -> " - " ++ method ++ " " ++ url) pendingRequests
|
||||
]
|
||||
]
|
||||
|
||||
MultipleMatchingHttpRequest expected actual functionName request pendingRequests ->
|
||||
String.concat
|
||||
[ functionName
|
||||
, ": "
|
||||
, "Expected "
|
||||
, case expected of
|
||||
1 ->
|
||||
"a single HTTP request"
|
||||
|
||||
_ ->
|
||||
String.fromInt expected ++ " HTTP requests"
|
||||
, " ("
|
||||
, request.method
|
||||
, " "
|
||||
, request.url
|
||||
, ") to have been made, but "
|
||||
, String.fromInt actual
|
||||
, " such requests were made.\n"
|
||||
, case pendingRequests of
|
||||
[] ->
|
||||
" No requests were made."
|
||||
|
||||
_ ->
|
||||
String.concat
|
||||
[ " The following requests were made:\n"
|
||||
, String.join "\n" <|
|
||||
List.map (\( method, url ) -> " - " ++ method ++ " " ++ url) pendingRequests
|
||||
]
|
||||
, if expected == 1 && actual > 1 then
|
||||
let
|
||||
useInstead =
|
||||
if String.startsWith "simulate" functionName then
|
||||
"simulateHttpResponseAdvanced"
|
||||
|
||||
else if String.startsWith "expect" functionName then
|
||||
"expectHttpRequests"
|
||||
|
||||
else
|
||||
"ensureHttpRequests"
|
||||
in
|
||||
"\n\nNOTE: If you want to allow multiple requests to the same endpoint, use ProgramTest." ++ useInstead ++ "."
|
||||
|
||||
else
|
||||
""
|
||||
]
|
||||
|
||||
EffectSimulationNotConfigured functionName ->
|
||||
"TEST SETUP ERROR: In order to use " ++ functionName ++ ", you MUST use ProgramTest.withSimulatedEffects before calling ProgramTest.start"
|
||||
|
||||
ViewAssertionFailed functionName html highlight reason ->
|
||||
let
|
||||
highlighter =
|
||||
if Set.isEmpty highlight then
|
||||
\_ _ _ -> True
|
||||
|
||||
else
|
||||
\tag attrs children ->
|
||||
Set.member tag highlight
|
||||
in
|
||||
String.join "\n"
|
||||
[ TestHtmlHacks.renderHtml showColors.dim highlighter (Query.fromHtml html)
|
||||
, ""
|
||||
, "▼ " ++ functionName
|
||||
, ""
|
||||
, renderQueryFailureWithContext renderQueryFailure 0 True reason
|
||||
]
|
||||
|
||||
CustomFailure assertionName message ->
|
||||
assertionName ++ ": " ++ message
|
||||
|
||||
|
||||
renderQueryFailureWithContext : (Int -> Bool -> a -> String) -> Int -> Bool -> ( ComplexQuery.FailureContext, a ) -> String
|
||||
renderQueryFailureWithContext renderInner indent color failure =
|
||||
let
|
||||
indentS =
|
||||
String.repeat indent " "
|
||||
in
|
||||
case failure of
|
||||
( [], inner ) ->
|
||||
renderInner indent color inner
|
||||
|
||||
( (Description description) :: baseFailure, inner ) ->
|
||||
String.join "\n" <|
|
||||
List.filter ((/=) "")
|
||||
[ indentS ++ renderDescriptionResult (colorsFor color) description ++ ":"
|
||||
, renderQueryFailureWithContext renderInner (indent + 2) color ( baseFailure, inner )
|
||||
]
|
||||
|
||||
( (CheckSucceeded description checkContext) :: baseFailure, inner ) ->
|
||||
String.join "\n" <|
|
||||
List.filter ((/=) "")
|
||||
[ indentS ++ renderDescriptionResult (colorsFor color) (Ok description) ++ ":"
|
||||
, renderQueryFailureWithContext_ (\_ _ () -> "") (indent + 2) color ( checkContext, () )
|
||||
, renderQueryFailureWithContext renderInner indent color ( baseFailure, inner )
|
||||
]
|
||||
|
||||
( (FindSucceeded (Just description) successfulChecks) :: baseFailure, inner ) ->
|
||||
String.join "\n" <|
|
||||
List.filter ((/=) "")
|
||||
[ indentS ++ renderDescriptionResult (colorsFor color) (Ok description) ++ ":"
|
||||
, renderSelectorResults (indent + 2) (colorsFor color) (List.map Ok (successfulChecks ()))
|
||||
, renderQueryFailureWithContext renderInner indent color ( baseFailure, inner )
|
||||
]
|
||||
|
||||
( (FindSucceeded Nothing successfulChecks) :: baseFailure, inner ) ->
|
||||
String.join "\n" <|
|
||||
List.filter ((/=) "")
|
||||
[ renderSelectorResults indent (colorsFor color) (List.map Ok (successfulChecks ()))
|
||||
, renderQueryFailureWithContext renderInner indent color ( baseFailure, inner )
|
||||
]
|
||||
|
||||
|
||||
renderQueryFailureWithContext_ : (Int -> Bool -> a -> String) -> Int -> Bool -> ( ComplexQuery.FailureContext, a ) -> String
|
||||
renderQueryFailureWithContext_ =
|
||||
renderQueryFailureWithContext
|
||||
|
||||
|
||||
renderQueryFailure : Int -> Bool -> ComplexQuery.Failure -> String
|
||||
renderQueryFailure indent color failure =
|
||||
let
|
||||
indentS =
|
||||
String.repeat indent " "
|
||||
in
|
||||
case failure of
|
||||
QueryFailed failureReason ->
|
||||
renderSelectorResults indent (colorsFor color) failureReason
|
||||
|
||||
ComplexQuery.SimulateFailed string ->
|
||||
let
|
||||
colors =
|
||||
colorsFor color
|
||||
in
|
||||
indentS ++ renderSelectorResult colors (Err string)
|
||||
|
||||
NoMatches description options ->
|
||||
let
|
||||
sortedByPriority =
|
||||
options
|
||||
|> List.sortBy (\( _, prio, _ ) -> -prio)
|
||||
|
||||
maxPriority =
|
||||
List.head sortedByPriority
|
||||
|> Maybe.map (\( _, prio, _ ) -> prio)
|
||||
|> Maybe.withDefault 0
|
||||
in
|
||||
String.join "\n" <|
|
||||
List.concat
|
||||
[ [ indentS ++ description ++ ":" ]
|
||||
, sortedByPriority
|
||||
|> List.filter (\( _, prio, _ ) -> prio > maxPriority - 2)
|
||||
|> List.map (\( desc, prio, reason ) -> indentS ++ "- " ++ desc ++ "\n" ++ renderQueryFailureWithContext renderQueryFailure (indent + 4) (color && prio >= maxPriority - 1) reason)
|
||||
]
|
||||
|
||||
TooManyMatches description matches ->
|
||||
String.join "\n" <|
|
||||
List.concat
|
||||
[ [ indentS ++ description ++ ", but there were multiple successful matches:" ]
|
||||
, matches
|
||||
|> List.sortBy (\( _, prio, _ ) -> -prio)
|
||||
|> List.map (\( desc, _, todo ) -> indentS ++ "- " ++ desc)
|
||||
, [ ""
|
||||
, "If that's what you intended, use `ProgramTest.within` to focus in on a portion of"
|
||||
, "the view that contains only one of the matches."
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
renderSelectorResults : Int -> Colors -> List (Result String String) -> String
|
||||
renderSelectorResults indent colors results =
|
||||
let
|
||||
indentS =
|
||||
String.repeat indent " "
|
||||
in
|
||||
List.map ((++) indentS << renderSelectorResult colors) (upToFirstErr results)
|
||||
|> String.join "\n"
|
||||
|
||||
|
||||
renderSelectorResult : Colors -> Result String String -> String
|
||||
renderSelectorResult colors result =
|
||||
case result of
|
||||
Ok selector ->
|
||||
String.concat
|
||||
[ colors.green "✓"
|
||||
, " "
|
||||
, colors.bold selector
|
||||
]
|
||||
|
||||
Err selector ->
|
||||
colors.red <|
|
||||
String.concat
|
||||
[ "✗"
|
||||
, " "
|
||||
, selector
|
||||
]
|
||||
|
||||
|
||||
renderDescriptionResult : Colors -> Result String String -> String
|
||||
renderDescriptionResult colors result =
|
||||
case result of
|
||||
Ok selector ->
|
||||
String.concat
|
||||
[ colors.green "✓"
|
||||
, " "
|
||||
, selector
|
||||
]
|
||||
|
||||
Err selector ->
|
||||
String.concat
|
||||
[ colors.red "✗"
|
||||
, " "
|
||||
, selector
|
||||
]
|
||||
|
||||
|
||||
upToFirstErr : List (Result x a) -> List (Result x a)
|
||||
upToFirstErr results =
|
||||
let
|
||||
step acc results_ =
|
||||
case results_ of
|
||||
[] ->
|
||||
acc
|
||||
|
||||
(Err x) :: _ ->
|
||||
Err x :: acc
|
||||
|
||||
(Ok a) :: rest ->
|
||||
step (Ok a :: acc) rest
|
||||
in
|
||||
step [] results
|
||||
|> List.reverse
|
||||
|
||||
|
||||
type alias Colors =
|
||||
{ bold : String -> String
|
||||
, red : String -> String
|
||||
, green : String -> String
|
||||
, dim : String -> String
|
||||
}
|
||||
|
||||
|
||||
colorsFor : Bool -> Colors
|
||||
colorsFor show =
|
||||
if show then
|
||||
showColors
|
||||
|
||||
else
|
||||
noColors
|
||||
|
||||
|
||||
showColors : Colors
|
||||
showColors =
|
||||
{ bold = \s -> String.concat [ "\u{001B}[1m", s, "\u{001B}[0m" ]
|
||||
, red = \s -> String.concat [ "\u{001B}[31m", s, "\u{001B}[0m" ]
|
||||
, green = \s -> String.concat [ "\u{001B}[32m", s, "\u{001B}[0m" ]
|
||||
, dim = \s -> String.concat [ "\u{001B}[2m", s, "\u{001B}[0m" ]
|
||||
}
|
||||
|
||||
|
||||
noColors : Colors
|
||||
noColors =
|
||||
{ bold = identity
|
||||
, red = identity
|
||||
, green = identity
|
||||
, dim = identity
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
module ProgramTest.HtmlHighlighter exposing (Attribute, Node(..), NodeF(..), fold, foldWithOriginal, highlight, isNonHiddenElement)
|
||||
|
||||
import Html.Parser
|
||||
|
||||
|
||||
type NodeF a
|
||||
= TextF String
|
||||
| ElementF String (List Attribute) (List a)
|
||||
| CommentF String
|
||||
|
||||
|
||||
type alias Attribute =
|
||||
( String, String )
|
||||
|
||||
|
||||
fold : (NodeF a -> a) -> Html.Parser.Node -> a
|
||||
fold f node =
|
||||
case node of
|
||||
Html.Parser.Text text ->
|
||||
f (TextF text)
|
||||
|
||||
Html.Parser.Element tag attrs children ->
|
||||
f (ElementF tag attrs (List.map (fold f) children))
|
||||
|
||||
Html.Parser.Comment string ->
|
||||
f (CommentF string)
|
||||
|
||||
|
||||
foldWithOriginal : (NodeF ( Html.Parser.Node, a ) -> a) -> Html.Parser.Node -> a
|
||||
foldWithOriginal f node =
|
||||
case node of
|
||||
Html.Parser.Text text ->
|
||||
f (TextF text)
|
||||
|
||||
Html.Parser.Element tag attrs children ->
|
||||
f (ElementF tag attrs (List.map (\child -> ( child, foldWithOriginal f child )) children))
|
||||
|
||||
Html.Parser.Comment string ->
|
||||
f (CommentF string)
|
||||
|
||||
|
||||
type Node
|
||||
= Text String
|
||||
| Element String (List Attribute) (List Node)
|
||||
| Comment String
|
||||
| Hidden String
|
||||
|
||||
|
||||
highlight : (String -> List Attribute -> List Html.Parser.Node -> Bool) -> Html.Parser.Node -> Node
|
||||
highlight predicate =
|
||||
foldWithOriginal <|
|
||||
\node ->
|
||||
case node of
|
||||
TextF text ->
|
||||
Text text
|
||||
|
||||
ElementF tag attrs children ->
|
||||
let
|
||||
foldedChildren =
|
||||
List.map Tuple.second children
|
||||
in
|
||||
if predicate tag attrs (List.map Tuple.first children) || List.any isNonHiddenElement foldedChildren then
|
||||
Element tag attrs foldedChildren
|
||||
|
||||
else
|
||||
let
|
||||
bestId =
|
||||
List.concatMap identity
|
||||
[ List.filter (Tuple.first >> (==) "id") attrs
|
||||
, List.filter (Tuple.first >> (==) "name") attrs
|
||||
, List.filter (Tuple.first >> (==) "class") attrs
|
||||
]
|
||||
|> List.head
|
||||
|> Maybe.map (\( name, value ) -> " " ++ name ++ "=\"" ++ value ++ "\"")
|
||||
|> Maybe.withDefault ""
|
||||
|
||||
bestContent =
|
||||
case foldedChildren of
|
||||
[] ->
|
||||
""
|
||||
|
||||
[ Text single ] ->
|
||||
truncate 15 (String.trim single)
|
||||
|
||||
_ ->
|
||||
"..."
|
||||
in
|
||||
Hidden ("<" ++ tag ++ bestId ++ ">" ++ bestContent ++ "</" ++ tag ++ ">")
|
||||
|
||||
CommentF string ->
|
||||
Comment string
|
||||
|
||||
|
||||
isNonHiddenElement : Node -> Bool
|
||||
isNonHiddenElement node =
|
||||
case node of
|
||||
Text _ ->
|
||||
False
|
||||
|
||||
Element _ _ _ ->
|
||||
True
|
||||
|
||||
Comment _ ->
|
||||
False
|
||||
|
||||
Hidden _ ->
|
||||
False
|
||||
|
||||
|
||||
truncate : Int -> String -> String
|
||||
truncate max input =
|
||||
if String.length input < max - 3 then
|
||||
input
|
||||
|
||||
else
|
||||
String.left (max - 3) input ++ "..."
|
@ -0,0 +1,58 @@
|
||||
module ProgramTest.HtmlParserHacks exposing (parse, trimText)
|
||||
|
||||
import Html.Parser
|
||||
import Parser
|
||||
import ProgramTest.StringLines as StringLines
|
||||
|
||||
|
||||
parse : String -> Result (List Parser.DeadEnd) (List Html.Parser.Node)
|
||||
parse input =
|
||||
case Html.Parser.run input of
|
||||
Ok nodes ->
|
||||
Ok nodes
|
||||
|
||||
Err errs ->
|
||||
case fixError errs input of
|
||||
Nothing ->
|
||||
Err errs
|
||||
|
||||
Just nodes ->
|
||||
Ok nodes
|
||||
|
||||
|
||||
fixError : List Parser.DeadEnd -> String -> Maybe (List Html.Parser.Node)
|
||||
fixError errs input =
|
||||
case errs of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
{ row, col, problem } :: rest ->
|
||||
case problem of
|
||||
Parser.UnexpectedChar ->
|
||||
case StringLines.charAt row (col - 1) input of
|
||||
Just "<" ->
|
||||
parse (StringLines.replaceAt row (col - 1) "<" input)
|
||||
|> Result.toMaybe
|
||||
|
||||
_ ->
|
||||
fixError rest input
|
||||
|
||||
_ ->
|
||||
fixError rest input
|
||||
|
||||
|
||||
trimText : Html.Parser.Node -> Html.Parser.Node
|
||||
trimText node =
|
||||
case node of
|
||||
Html.Parser.Text string ->
|
||||
Html.Parser.Text (String.trim string)
|
||||
|
||||
Html.Parser.Element string list nodes ->
|
||||
Html.Parser.Element string
|
||||
list
|
||||
(List.map trimText nodes
|
||||
|> List.filter ((/=) (Html.Parser.Text ""))
|
||||
)
|
||||
|
||||
Html.Parser.Comment string ->
|
||||
Html.Parser.Comment string
|
@ -0,0 +1,73 @@
|
||||
module ProgramTest.HtmlRenderer exposing (render)
|
||||
|
||||
import ProgramTest.HtmlHighlighter as HtmlHighlighter exposing (Node(..))
|
||||
|
||||
|
||||
render : (String -> String) -> Int -> List HtmlHighlighter.Node -> String
|
||||
render colorHidden indent nodes =
|
||||
case nodes of
|
||||
[] ->
|
||||
""
|
||||
|
||||
(Text text) :: rest ->
|
||||
case String.trim (String.replace "\n" " " text) of
|
||||
"" ->
|
||||
render colorHidden indent rest
|
||||
|
||||
trimmed ->
|
||||
String.repeat indent " " ++ trimmed ++ "\n" ++ render colorHidden indent rest
|
||||
|
||||
(Comment text) :: rest ->
|
||||
String.repeat indent " " ++ "<!--" ++ text ++ "-->\n" ++ render colorHidden indent rest
|
||||
|
||||
(Element tag attrs []) :: rest ->
|
||||
String.repeat indent " "
|
||||
++ "<"
|
||||
++ tag
|
||||
++ renderAttrs attrs
|
||||
++ "></"
|
||||
++ tag
|
||||
++ ">\n"
|
||||
++ render colorHidden indent rest
|
||||
|
||||
(Element tag attrs children) :: rest ->
|
||||
String.repeat indent " "
|
||||
++ "<"
|
||||
++ tag
|
||||
++ renderAttrs attrs
|
||||
++ ">\n"
|
||||
++ render colorHidden (indent + 4) children
|
||||
++ String.repeat indent " "
|
||||
++ "</"
|
||||
++ tag
|
||||
++ ">\n"
|
||||
++ render colorHidden indent rest
|
||||
|
||||
(Hidden short) :: rest ->
|
||||
String.repeat indent " " ++ colorHidden short ++ "\n" ++ render colorHidden indent rest
|
||||
|
||||
|
||||
renderAttrs : List HtmlHighlighter.Attribute -> String
|
||||
renderAttrs attrs =
|
||||
case attrs of
|
||||
[] ->
|
||||
""
|
||||
|
||||
some ->
|
||||
" " ++ String.join " " (List.map renderAttr some)
|
||||
|
||||
|
||||
renderAttr : ( String, String ) -> String
|
||||
renderAttr ( name, value ) =
|
||||
case ( name, value ) of
|
||||
( "htmlfor", _ ) ->
|
||||
"for=\"" ++ value ++ "\""
|
||||
|
||||
( _, "true" ) ->
|
||||
name ++ "=true"
|
||||
|
||||
( _, "false" ) ->
|
||||
name ++ "=false"
|
||||
|
||||
_ ->
|
||||
name ++ "=\"" ++ value ++ "\""
|
@ -0,0 +1,28 @@
|
||||
module ProgramTest.Program exposing (Program, renderView)
|
||||
|
||||
import Html exposing (Html)
|
||||
import Test.Html.Query as Query
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
{-| Since we can't inspect `Platform.Program`s in Elm,
|
||||
this type represents the same thing as a record that we can access.
|
||||
|
||||
Note that we also parameterize `effect` and `sub` separately because
|
||||
`Platform.Cmd` and `Platform.Sub` are not inspectable in Elm.
|
||||
|
||||
-}
|
||||
type alias Program model msg effect sub =
|
||||
{ update : msg -> model -> ( model, effect )
|
||||
, view : model -> Html msg
|
||||
, onRouteChange : Url -> Maybe msg
|
||||
, subscriptions : Maybe (model -> sub)
|
||||
, withinFocus : Query.Single msg -> Query.Single msg
|
||||
}
|
||||
|
||||
|
||||
renderView : Program model msg effect sub -> model -> Query.Single msg
|
||||
renderView program model =
|
||||
program.view model
|
||||
|> Query.fromHtml
|
||||
|> program.withinFocus
|
@ -0,0 +1,26 @@
|
||||
module ProgramTest.StringLines exposing (charAt, replaceAt)
|
||||
|
||||
|
||||
charAt : Int -> Int -> String -> Maybe String
|
||||
charAt row col input =
|
||||
String.lines input
|
||||
|> List.drop (row - 1)
|
||||
|> List.head
|
||||
|> Maybe.map
|
||||
(String.dropLeft (col - 1)
|
||||
>> String.left 1
|
||||
)
|
||||
|
||||
|
||||
replaceAt : Int -> Int -> String -> String -> String
|
||||
replaceAt row col replacement input =
|
||||
String.lines input
|
||||
|> List.indexedMap
|
||||
(\i line ->
|
||||
if i == (row - 1) then
|
||||
String.left (col - 1) line ++ replacement ++ String.dropLeft col line
|
||||
|
||||
else
|
||||
line
|
||||
)
|
||||
|> String.join "\n"
|
@ -0,0 +1,132 @@
|
||||
module ProgramTest.TestHtmlHacks exposing (getPassingSelectors, parseFailureReport, parseFailureReportWithoutHtml, parseSimulateFailure, renderHtml)
|
||||
|
||||
import Html.Parser
|
||||
import Parser
|
||||
import Parser.Extra
|
||||
import ProgramTest.HtmlHighlighter as HtmlHighlighter
|
||||
import ProgramTest.HtmlRenderer as HtmlRenderer
|
||||
import ProgramTest.TestHtmlParser as TestHtmlParser exposing (Assertion(..), FailureReport(..))
|
||||
import Test.Html.Query as Query
|
||||
import Test.Html.Selector as Selector exposing (Selector)
|
||||
import Test.Runner
|
||||
|
||||
|
||||
pleaseReport description =
|
||||
"PLEASE REPORT THIS AT <https://github.com/avh4/elm-program-test/issues>: " ++ description
|
||||
|
||||
|
||||
renderHtml : (String -> String) -> (String -> List Html.Parser.Attribute -> List Html.Parser.Node -> Bool) -> Query.Single any -> String
|
||||
renderHtml colorHidden highlightPredicate single =
|
||||
case forceFailureReport [] single of
|
||||
Ok (QueryFailure node _ _) ->
|
||||
let
|
||||
tryHighlight =
|
||||
HtmlHighlighter.highlight highlightPredicate
|
||||
node
|
||||
|
||||
finalHighlighted =
|
||||
if HtmlHighlighter.isNonHiddenElement tryHighlight then
|
||||
tryHighlight
|
||||
|
||||
else
|
||||
HtmlHighlighter.highlight (\_ _ _ -> True)
|
||||
node
|
||||
in
|
||||
"▼ Query.fromHtml\n\n"
|
||||
++ HtmlRenderer.render colorHidden 4 [ finalHighlighted ]
|
||||
|
||||
Ok (EventFailure name _) ->
|
||||
pleaseReport ("renderHtml: unexpected EventFailure: \"" ++ name ++ "\"")
|
||||
|
||||
Err err ->
|
||||
pleaseReport ("renderHtml: couldn't parse failure report: " ++ err)
|
||||
|
||||
|
||||
getPassingSelectors : List Selector -> Query.Single msg -> List String
|
||||
getPassingSelectors selectors single =
|
||||
case forceFailureReportWithoutHtml selectors single of
|
||||
Ok (QueryFailure _ _ (Has _ results)) ->
|
||||
case List.reverse results of
|
||||
(Ok _) :: _ ->
|
||||
[ pleaseReport "getPassingSelectors: forced selector didn't fail" ]
|
||||
|
||||
_ ->
|
||||
List.filterMap Result.toMaybe results
|
||||
|
||||
Ok (EventFailure name _) ->
|
||||
[ pleaseReport ("getPassingSelectors: got unexpected EventFailure \"" ++ name ++ "\"") ]
|
||||
|
||||
Err err ->
|
||||
[ pleaseReport ("getPassingSelectors: couldn't parse failure report: " ++ err) ]
|
||||
|
||||
|
||||
forceFailureReport : List Selector -> Query.Single any -> Result String (FailureReport Html.Parser.Node)
|
||||
forceFailureReport selectors =
|
||||
forceFailureReport_ parseFailureReport selectors "ProgramTest.TestHtmlHacks is trying to force a failure to collect the error message %%"
|
||||
|
||||
|
||||
forceFailureReportWithoutHtml : List Selector -> Query.Single any -> Result String (FailureReport ())
|
||||
forceFailureReportWithoutHtml selectors =
|
||||
forceFailureReport_ parseFailureReportWithoutHtml selectors "ProgramTest.TestHtmlHacks is trying to force a failure to collect the error message %%"
|
||||
|
||||
|
||||
forceFailureReport_ : (String -> result) -> List Selector -> String -> Query.Single any -> result
|
||||
forceFailureReport_ parseFailure selectors unique single =
|
||||
case
|
||||
single
|
||||
|> Query.has (selectors ++ [ Selector.text unique ])
|
||||
|> Test.Runner.getFailureReason
|
||||
of
|
||||
Nothing ->
|
||||
-- We expect the fake query to fail -- if it doesn't for some reason, just try recursing with a different fake matching string until it does fail
|
||||
forceFailureReport_ parseFailure selectors (unique ++ "_") single
|
||||
|
||||
Just reason ->
|
||||
parseFailure reason.description
|
||||
|
||||
|
||||
parseFailureReport : String -> Result String (FailureReport Html.Parser.Node)
|
||||
parseFailureReport string =
|
||||
Parser.run TestHtmlParser.parser string
|
||||
|> Result.mapError Parser.Extra.deadEndsToString
|
||||
|
||||
|
||||
parseFailureReportWithoutHtml : String -> Result String (FailureReport ())
|
||||
parseFailureReportWithoutHtml string =
|
||||
Parser.run TestHtmlParser.parserWithoutHtml string
|
||||
|> Result.mapError Parser.Extra.deadEndsToString
|
||||
|
||||
|
||||
partitionSections_ : List String -> List (List String) -> List String -> List (List String)
|
||||
partitionSections_ accLines accSections remaining =
|
||||
case remaining of
|
||||
[] ->
|
||||
case List.reverse (List.reverse accLines :: accSections) of
|
||||
[] :: rest ->
|
||||
rest
|
||||
|
||||
all ->
|
||||
all
|
||||
|
||||
next :: rest ->
|
||||
if String.startsWith "▼ " next then
|
||||
partitionSections_ [ next ] (List.reverse accLines :: accSections) rest
|
||||
|
||||
else
|
||||
partitionSections_ (next :: accLines) accSections rest
|
||||
|
||||
|
||||
parseSimulateFailure : String -> String
|
||||
parseSimulateFailure string =
|
||||
let
|
||||
simpleFailure result =
|
||||
case result of
|
||||
EventFailure name html ->
|
||||
Ok ("Event.expectEvent: I found a node, but it does not listen for \"" ++ name ++ "\" events like I expected it would.")
|
||||
|
||||
_ ->
|
||||
Err (pleaseReport "Got a failure message from Test.Html.Query that we couldn't parse: " ++ string)
|
||||
in
|
||||
parseFailureReport string
|
||||
|> Result.andThen simpleFailure
|
||||
|> Result.withDefault (pleaseReport "Got a failure message from Test.Html.Query that we couldn't parse: " ++ string)
|
@ -0,0 +1,201 @@
|
||||
module ProgramTest.TestHtmlParser exposing (Assertion(..), FailureReport(..), Selector(..), Step(..), parser, parserWithoutHtml)
|
||||
|
||||
import Html.Parser
|
||||
import Parser exposing ((|.), (|=), Parser)
|
||||
import Parser.Extra.String
|
||||
import ProgramTest.HtmlParserHacks as HtmlParserHacks
|
||||
|
||||
|
||||
type FailureReport html
|
||||
= QueryFailure html (List (Step html)) Assertion
|
||||
| EventFailure String html
|
||||
|
||||
|
||||
type Step html
|
||||
= FindStep (List Selector) html
|
||||
|
||||
|
||||
type Selector
|
||||
= Tag String
|
||||
| Containing (List Selector)
|
||||
| Text String
|
||||
| Attribute String String
|
||||
| All (List Selector)
|
||||
|
||||
|
||||
type Assertion
|
||||
= Has (List Selector) (List (Result String String))
|
||||
|
||||
|
||||
parser_ : Parser html -> Parser (FailureReport html)
|
||||
parser_ parseHtml =
|
||||
Parser.oneOf
|
||||
[ Parser.succeed QueryFailure
|
||||
|. Parser.keyword "▼ Query.fromHtml"
|
||||
|. Parser.symbol "\n\n "
|
||||
|= parseHtml
|
||||
|= stepsParser parseHtml
|
||||
|= assertionParser
|
||||
|. Parser.end
|
||||
, Parser.succeed EventFailure
|
||||
|. Parser.keyword "Event.expectEvent:"
|
||||
|. Parser.symbol " I found a node, but it does not listen for \""
|
||||
|= (Parser.getChompedString <| Parser.chompUntil "\"")
|
||||
|. Parser.symbol "\" events like I expected it would.\n\n"
|
||||
|= parseHtml
|
||||
|. Parser.end
|
||||
]
|
||||
|
||||
|
||||
parser : Parser (FailureReport Html.Parser.Node)
|
||||
parser =
|
||||
parser_ trimmedHtml
|
||||
|
||||
|
||||
trimmedHtml : Parser Html.Parser.Node
|
||||
trimmedHtml =
|
||||
Parser.map HtmlParserHacks.trimText Html.Parser.node
|
||||
|. Parser.oneOf
|
||||
[ Parser.symbol "\n\n\n"
|
||||
, Parser.end
|
||||
]
|
||||
|
||||
|
||||
parserWithoutHtml : Parser (FailureReport ())
|
||||
parserWithoutHtml =
|
||||
parser_ ignoreHtml
|
||||
|
||||
|
||||
ignoreHtml : Parser ()
|
||||
ignoreHtml =
|
||||
Parser.chompUntilEndOr "▼"
|
||||
|
||||
|
||||
stepsParser : Parser html -> Parser (List (Step html))
|
||||
stepsParser parseHtml =
|
||||
Parser.loop [] <|
|
||||
\acc ->
|
||||
Parser.oneOf
|
||||
[ Parser.succeed (\stmt -> Parser.Loop (stmt :: acc))
|
||||
|= stepParser parseHtml
|
||||
, Parser.succeed ()
|
||||
|> Parser.map (\_ -> Parser.Done (List.reverse acc))
|
||||
]
|
||||
|
||||
|
||||
stepParser : Parser html -> Parser (Step html)
|
||||
stepParser parseHtml =
|
||||
Parser.oneOf
|
||||
[ Parser.succeed FindStep
|
||||
|. Parser.keyword "▼ Query.find "
|
||||
|= selectorsParser
|
||||
|. Parser.symbol "\n\n 1) "
|
||||
|= parseHtml
|
||||
]
|
||||
|
||||
|
||||
selectorsParser : Parser (List Selector)
|
||||
selectorsParser =
|
||||
Parser.sequence
|
||||
{ start = "[ "
|
||||
, separator = ", "
|
||||
, end = " ]"
|
||||
, spaces = Parser.succeed ()
|
||||
, item = selectorParser
|
||||
, trailing = Parser.Forbidden
|
||||
}
|
||||
|
||||
|
||||
selectorParser : Parser Selector
|
||||
selectorParser =
|
||||
-- As of elm-explorations/test 1.2.2, `Selector.all` renders simply as a space-separated sequence of selectors
|
||||
let
|
||||
done acc =
|
||||
case acc of
|
||||
[ single ] ->
|
||||
single
|
||||
|
||||
more ->
|
||||
All (List.reverse more)
|
||||
in
|
||||
singleSelectorParser
|
||||
|> Parser.andThen
|
||||
(\first ->
|
||||
Parser.loop [ first ] <|
|
||||
\acc ->
|
||||
Parser.oneOf
|
||||
[ Parser.succeed (\stmt -> Parser.Loop (stmt :: acc))
|
||||
|. Parser.backtrackable (Parser.symbol " ")
|
||||
|= singleSelectorParser
|
||||
, Parser.succeed ()
|
||||
|> Parser.map (\_ -> Parser.Done (done acc))
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
singleSelectorParser : Parser Selector
|
||||
singleSelectorParser =
|
||||
Parser.oneOf
|
||||
[ Parser.succeed Tag
|
||||
|. Parser.keyword "tag "
|
||||
|= Parser.Extra.String.string
|
||||
, Parser.succeed Text
|
||||
|. Parser.keyword "text "
|
||||
|= Parser.Extra.String.string
|
||||
, Parser.succeed Attribute
|
||||
|. Parser.keyword "attribute "
|
||||
|= Parser.Extra.String.string
|
||||
|. Parser.symbol " "
|
||||
|= Parser.oneOf
|
||||
[ Parser.Extra.String.string
|
||||
, Parser.succeed "true"
|
||||
|. Parser.keyword "True"
|
||||
, Parser.succeed "false"
|
||||
|. Parser.keyword "False"
|
||||
]
|
||||
, Parser.succeed Containing
|
||||
|. Parser.keyword "containing "
|
||||
|= Parser.lazy (\() -> selectorsParser)
|
||||
|. Parser.symbol " "
|
||||
]
|
||||
|
||||
|
||||
assertionParser : Parser Assertion
|
||||
assertionParser =
|
||||
Parser.oneOf
|
||||
[ Parser.succeed Has
|
||||
|. Parser.keyword "▼ Query.has "
|
||||
|= selectorsParser
|
||||
|. Parser.symbol "\n\n"
|
||||
|= selectorResultsParser
|
||||
]
|
||||
|
||||
|
||||
selectorResultsParser : Parser (List (Result String String))
|
||||
selectorResultsParser =
|
||||
let
|
||||
help acc =
|
||||
Parser.oneOf
|
||||
[ Parser.succeed (\stmt -> Parser.Loop (stmt :: acc))
|
||||
|= selectorResultParser
|
||||
, Parser.succeed ()
|
||||
|> Parser.map (\_ -> Parser.Done (List.reverse acc))
|
||||
]
|
||||
in
|
||||
Parser.loop [] help
|
||||
|
||||
|
||||
selectorResultParser : Parser (Result String String)
|
||||
selectorResultParser =
|
||||
Parser.oneOf
|
||||
[ Parser.succeed (Ok << String.trim)
|
||||
|. Parser.symbol "✓ "
|
||||
|. Parser.commit ()
|
||||
|= (Parser.getChompedString <| Parser.chompUntilEndOr "\n")
|
||||
|. Parser.chompWhile ((==) '\n')
|
||||
, Parser.succeed (Err << String.trim)
|
||||
|. Parser.symbol "✗ "
|
||||
|. Parser.commit ()
|
||||
|= (Parser.getChompedString <| Parser.chompUntilEndOr "\n")
|
||||
|. Parser.chompWhile ((==) '\n')
|
||||
]
|
55
examples/end-to-end/elm-program-test-src/Query/Extra.elm
Normal file
55
examples/end-to-end/elm-program-test-src/Query/Extra.elm
Normal file
@ -0,0 +1,55 @@
|
||||
module Query.Extra exposing (oneOf)
|
||||
|
||||
import Html
|
||||
import Test.Html.Query as Query
|
||||
import Test.Html.Selector as Selector
|
||||
import Test.Runner
|
||||
|
||||
|
||||
{-| This is a hack for the fact that elm-html-test does not provide a way to force a `Query.Single` into an error state.
|
||||
-}
|
||||
fail : String -> Query.Single msg -> Query.Single msg
|
||||
fail message _ =
|
||||
Html.text ("ERROR: " ++ message)
|
||||
|> Query.fromHtml
|
||||
|> Query.find [ Selector.text "SHOULD NOT HAVE ERROR" ]
|
||||
|
||||
|
||||
{-| Determines whether the given `Query.Single` is an error (failed to match a node)
|
||||
-}
|
||||
isFailed : Query.Single msg -> Bool
|
||||
isFailed single =
|
||||
case single |> Query.has [] |> Test.Runner.getFailureReason of
|
||||
Just _ ->
|
||||
True
|
||||
|
||||
Nothing ->
|
||||
False
|
||||
|
||||
|
||||
{-| TODO: Is it strange that this takes a `List (Single -> Single)`? Is it safer or more sensible to take `List (List Selector)` and then implicily only work with `Query.find`?
|
||||
-}
|
||||
oneOf : List (Query.Single msg -> Query.Single msg) -> Query.Single msg -> Query.Single msg
|
||||
oneOf options single =
|
||||
if isFailed single then
|
||||
-- the input single is an error, so just return that
|
||||
single
|
||||
|
||||
else
|
||||
case options of
|
||||
[] ->
|
||||
fail "Query.Extra.oneOf was given an empty list of options" single
|
||||
|
||||
[ last ] ->
|
||||
-- this is the last option, so if it fails, we want to return that failure
|
||||
-- TODO: if the all failed, give a better error message about everything that failed
|
||||
single |> last
|
||||
|
||||
next :: rest1 :: rest ->
|
||||
if isFailed (next single) then
|
||||
-- this option failed, so try the remaining ones
|
||||
oneOf (rest1 :: rest) single
|
||||
|
||||
else
|
||||
-- this option passed, so return success
|
||||
next single
|
@ -0,0 +1,6 @@
|
||||
module String.Extra exposing (escape)
|
||||
|
||||
|
||||
escape : String -> String
|
||||
escape s =
|
||||
"\"" ++ s ++ "\""
|
145
examples/end-to-end/elm-program-test-src/Test/Http.elm
Normal file
145
examples/end-to-end/elm-program-test-src/Test/Http.elm
Normal file
@ -0,0 +1,145 @@
|
||||
module Test.Http exposing
|
||||
( expectJsonBody, HttpRequest, hasHeader
|
||||
, timeout, networkError, httpResponse
|
||||
)
|
||||
|
||||
{-| Convenience functions for testing HTTP requests.
|
||||
_Pull requests are welcome to add more useful functions._
|
||||
|
||||
|
||||
## Expectations
|
||||
|
||||
These functions provide some convenient checks that can be used with [`ProgramTest.expectHttpRequest`](ProgramTest#expectHttpRequest).
|
||||
|
||||
@docs expectJsonBody, HttpRequest, hasHeader
|
||||
|
||||
|
||||
## Responses
|
||||
|
||||
These are ways to easily make `Http.Response` values for use with [`ProgramTest.simulateHttpResponse`](ProgramTest#simulateHttpResponse).
|
||||
|
||||
@docs timeout, networkError, httpResponse
|
||||
|
||||
-}
|
||||
|
||||
import Dict exposing (Dict)
|
||||
import Expect exposing (Expectation)
|
||||
import Http
|
||||
import Json.Decode
|
||||
import SimulatedEffect exposing (SimulatedTask)
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias HttpRequest x a =
|
||||
{ method : String
|
||||
, url : String
|
||||
, body : String
|
||||
, headers : List ( String, String )
|
||||
, onRequestComplete : Http.Response String -> SimulatedTask x a
|
||||
}
|
||||
|
||||
|
||||
{-| A convenient way to check something about the request body of a pending HTTP request.
|
||||
|
||||
...
|
||||
|> ProgramTest.expectHttpRequest "POST"
|
||||
"https://example.com/ok"
|
||||
(Test.Http.expectJsonBody
|
||||
(Json.Decode.field "version" Json.Decode.string)
|
||||
(Expect.equal "3.1.5")
|
||||
)
|
||||
|
||||
-}
|
||||
expectJsonBody :
|
||||
Json.Decode.Decoder requestBody
|
||||
-> (requestBody -> Expectation)
|
||||
-> HttpRequest x a
|
||||
-> Expectation
|
||||
expectJsonBody decoder check request =
|
||||
case Json.Decode.decodeString decoder request.body of
|
||||
Err err ->
|
||||
Expect.fail ("expectJsonBody: Failed to decode HTTP request body: " ++ Json.Decode.errorToString err)
|
||||
|
||||
Ok responseBody ->
|
||||
check responseBody
|
||||
|
||||
|
||||
{-| Assert that the given HTTP request has the specified header.
|
||||
|
||||
...
|
||||
|> ProgramTest.expectHttpRequest "POST"
|
||||
"https://example.com/ok"
|
||||
(Test.Http.hasHeader "Content-Type" "application/json")
|
||||
|
||||
-}
|
||||
hasHeader : String -> String -> HttpRequest x a -> Expectation
|
||||
hasHeader key value { headers } =
|
||||
let
|
||||
key_ =
|
||||
String.toLower key
|
||||
|
||||
value_ =
|
||||
String.toLower value
|
||||
|
||||
matches ( k, v ) =
|
||||
( String.toLower k, String.toLower v )
|
||||
== ( key_, value_ )
|
||||
in
|
||||
if List.any matches headers then
|
||||
Expect.pass
|
||||
|
||||
else
|
||||
Expect.fail <|
|
||||
String.join "\n"
|
||||
[ "Expected HTTP header " ++ key ++ ": " ++ value
|
||||
, "but got headers:"
|
||||
, List.map (\( k, v ) -> " " ++ k ++ ": " ++ v) headers
|
||||
|> String.join "\n"
|
||||
]
|
||||
|
||||
|
||||
{-| This is the same as `Http.Timeout_`,
|
||||
but is exposed here so that your test doesn't need to import both `Http` and `Test.Http`.
|
||||
-}
|
||||
timeout : Http.Response body
|
||||
timeout =
|
||||
Http.Timeout_
|
||||
|
||||
|
||||
{-| This is the same as `Http.NetworkError_`,
|
||||
but is exposed here so that your test doesn't need to import both `Http` and `Test.Http`.
|
||||
-}
|
||||
networkError : Http.Response body
|
||||
networkError =
|
||||
Http.NetworkError_
|
||||
|
||||
|
||||
{-| This is a more convenient way to create `Http.BadStatus_` and `Http.GoodStatus_` values.
|
||||
|
||||
Following the [logic in elm/http](https://github.com/elm/http/blob/2.0.0/src/Elm/Kernel/Http.js#L65),
|
||||
this will produce `Http.GoodStatus_` if the given status code is in the 200 series, otherwise
|
||||
it will produce `Http.BadStatus_`.
|
||||
|
||||
-}
|
||||
httpResponse :
|
||||
{ statusCode : Int
|
||||
, headers : List ( String, String )
|
||||
, body : body
|
||||
}
|
||||
-> Http.Response body
|
||||
httpResponse response =
|
||||
let
|
||||
variant =
|
||||
if response.statusCode >= 200 && response.statusCode < 300 then
|
||||
Http.GoodStatus_
|
||||
|
||||
else
|
||||
Http.BadStatus_
|
||||
in
|
||||
variant
|
||||
{ url = ""
|
||||
, statusCode = response.statusCode
|
||||
, statusText = "TODO: if you need this, please report to https://github.com/avh4/elm-program-test/issues"
|
||||
, headers = Dict.fromList response.headers
|
||||
}
|
||||
response.body
|
35
examples/end-to-end/elm-program-test-src/TestResult.elm
Normal file
35
examples/end-to-end/elm-program-test-src/TestResult.elm
Normal file
@ -0,0 +1,35 @@
|
||||
module TestResult exposing (TestResult, andThen, fail)
|
||||
|
||||
import ProgramTest.Failure exposing (Failure)
|
||||
import TestState exposing (TestState)
|
||||
|
||||
|
||||
{-| TODO: what's a better name?
|
||||
-}
|
||||
type alias TestResult model msg effect =
|
||||
Result
|
||||
{ reason : Failure
|
||||
}
|
||||
(TestState model msg effect)
|
||||
|
||||
|
||||
fail : Failure -> TestState model msg effect -> TestResult model msg effect
|
||||
fail failure state =
|
||||
Err
|
||||
{ reason = failure
|
||||
}
|
||||
|
||||
|
||||
andThen : (TestState model msg effect -> Result Failure (TestState model msg effect)) -> TestResult model msg effect -> TestResult model msg effect
|
||||
andThen f testResult =
|
||||
case testResult of
|
||||
Ok state ->
|
||||
case f state of
|
||||
Err failure ->
|
||||
fail failure state
|
||||
|
||||
Ok newState ->
|
||||
Ok newState
|
||||
|
||||
Err _ ->
|
||||
testResult
|
285
examples/end-to-end/elm-program-test-src/TestState.elm
Normal file
285
examples/end-to-end/elm-program-test-src/TestState.elm
Normal file
@ -0,0 +1,285 @@
|
||||
module TestState exposing (TestState, advanceTime, drain, queueEffect, routeChangeHelper, simulateLoadUrlHelper, update, withSimulation)
|
||||
|
||||
import Dict
|
||||
import PairingHeap
|
||||
import ProgramTest.EffectSimulation as EffectSimulation exposing (EffectSimulation)
|
||||
import ProgramTest.Failure exposing (Failure(..))
|
||||
import ProgramTest.Program exposing (Program)
|
||||
import SimulatedEffect exposing (SimulatedEffect, SimulatedSub)
|
||||
import String.Extra
|
||||
import Url exposing (Url)
|
||||
import Url.Extra
|
||||
|
||||
|
||||
{-| TODO: what's a better name?
|
||||
-}
|
||||
type alias TestState model msg effect =
|
||||
{ currentModel : model
|
||||
, lastEffect : effect
|
||||
, navigation :
|
||||
Maybe
|
||||
{ currentLocation : Url
|
||||
, browserHistory : List Url
|
||||
}
|
||||
, effectSimulation : Maybe (EffectSimulation msg effect)
|
||||
}
|
||||
|
||||
|
||||
update : msg -> Program model msg effect sub -> TestState model msg effect -> Result Failure (TestState model msg effect)
|
||||
update msg program state =
|
||||
let
|
||||
( newModel, newEffect ) =
|
||||
program.update msg state.currentModel
|
||||
in
|
||||
{ state
|
||||
| currentModel = newModel
|
||||
, lastEffect = newEffect
|
||||
}
|
||||
|> queueEffect program newEffect
|
||||
|> Result.andThen (drain program)
|
||||
|
||||
|
||||
queueEffect : Program model msg effect sub -> effect -> TestState model msg effect -> Result Failure (TestState model msg effect)
|
||||
queueEffect program effect state =
|
||||
case state.effectSimulation of
|
||||
Nothing ->
|
||||
Ok state
|
||||
|
||||
Just simulation ->
|
||||
queueSimulatedEffect program (simulation.deconstructEffect effect) state
|
||||
|
||||
|
||||
queueSimulatedEffect : Program model msg effect sub -> SimulatedEffect msg -> TestState model msg effect -> Result Failure (TestState model msg effect)
|
||||
queueSimulatedEffect program effect state =
|
||||
case state.effectSimulation of
|
||||
Nothing ->
|
||||
Ok state
|
||||
|
||||
Just simulation ->
|
||||
case effect of
|
||||
SimulatedEffect.None ->
|
||||
Ok state
|
||||
|
||||
SimulatedEffect.Batch effects ->
|
||||
List.foldl (\ef -> Result.andThen (queueSimulatedEffect program ef)) (Ok state) effects
|
||||
|
||||
SimulatedEffect.Task t ->
|
||||
Ok
|
||||
{ state
|
||||
| effectSimulation =
|
||||
Just (EffectSimulation.queueTask t simulation)
|
||||
}
|
||||
|
||||
SimulatedEffect.PortEffect portName value ->
|
||||
Ok
|
||||
{ state
|
||||
| effectSimulation =
|
||||
Just
|
||||
{ simulation
|
||||
| outgoingPortValues =
|
||||
Dict.update portName
|
||||
(Maybe.withDefault [] >> (::) value >> Just)
|
||||
simulation.outgoingPortValues
|
||||
}
|
||||
}
|
||||
|
||||
SimulatedEffect.PushUrl url ->
|
||||
routeChangeHelper ("simulating effect: SimulatedEffect.Navigation.pushUrl " ++ String.Extra.escape url) 0 url program state
|
||||
|
||||
SimulatedEffect.ReplaceUrl url ->
|
||||
routeChangeHelper ("simulating effect: SimulatedEffect.Navigation.replaceUrl " ++ String.Extra.escape url) 1 url program state
|
||||
|
||||
SimulatedEffect.Back n ->
|
||||
case state.navigation of
|
||||
Nothing ->
|
||||
Ok state
|
||||
|
||||
Just { currentLocation, browserHistory } ->
|
||||
if n <= 0 then
|
||||
Ok state
|
||||
|
||||
else
|
||||
case List.head (List.drop (n - 1) browserHistory) of
|
||||
Nothing ->
|
||||
-- n is bigger than the history;
|
||||
-- in this case, browsers ignore the request
|
||||
Ok state
|
||||
|
||||
Just first ->
|
||||
routeChangeHelper ("simulating effect: SimulatedEffect.Navigation.Back " ++ String.fromInt n) 2 (Url.toString first) program state
|
||||
|
||||
SimulatedEffect.Load url ->
|
||||
Err (simulateLoadUrlHelper ("simulating effect: SimulatedEffect.Navigation.load " ++ url) url state)
|
||||
|
||||
SimulatedEffect.Reload skipCache ->
|
||||
let
|
||||
functionName =
|
||||
if skipCache then
|
||||
"reloadAndSkipCache"
|
||||
|
||||
else
|
||||
"reload"
|
||||
in
|
||||
case state.navigation of
|
||||
Nothing ->
|
||||
Err (ProgramDoesNotSupportNavigation functionName)
|
||||
|
||||
Just { currentLocation } ->
|
||||
Err (ChangedPage ("simulating effect: SimulatedEffect.Navigation." ++ functionName) currentLocation)
|
||||
|
||||
|
||||
simulateLoadUrlHelper : String -> String -> TestState model msg effect -> Failure
|
||||
simulateLoadUrlHelper functionDescription href state =
|
||||
case Maybe.map .currentLocation state.navigation of
|
||||
Just location ->
|
||||
ChangedPage functionDescription (Url.Extra.resolve location href)
|
||||
|
||||
Nothing ->
|
||||
case Url.fromString href of
|
||||
Nothing ->
|
||||
NoBaseUrl functionDescription href
|
||||
|
||||
Just location ->
|
||||
ChangedPage functionDescription location
|
||||
|
||||
|
||||
routeChangeHelper : String -> Int -> String -> Program model msg effect sub -> TestState model msg effect -> Result Failure (TestState model msg effect)
|
||||
routeChangeHelper functionName removeFromBackStack url program state =
|
||||
case state.navigation of
|
||||
Nothing ->
|
||||
Err (ProgramDoesNotSupportNavigation functionName)
|
||||
|
||||
Just { currentLocation, browserHistory } ->
|
||||
let
|
||||
newLocation =
|
||||
Url.Extra.resolve currentLocation url
|
||||
|
||||
processRouteChange =
|
||||
case program.onRouteChange newLocation of
|
||||
Nothing ->
|
||||
Ok
|
||||
|
||||
Just msg ->
|
||||
-- TODO: should this be set before or after?
|
||||
update msg program
|
||||
in
|
||||
{ state
|
||||
| navigation =
|
||||
Just
|
||||
{ currentLocation = newLocation
|
||||
, browserHistory =
|
||||
(currentLocation :: browserHistory)
|
||||
|> List.drop removeFromBackStack
|
||||
}
|
||||
}
|
||||
|> processRouteChange
|
||||
|
||||
|
||||
drain : Program model msg effect sub -> TestState model msg effect -> Result Failure (TestState model msg effect)
|
||||
drain program =
|
||||
let
|
||||
advanceTimeIfSimulating t state =
|
||||
case state.effectSimulation of
|
||||
Nothing ->
|
||||
Ok state
|
||||
|
||||
Just _ ->
|
||||
advanceTime "<UNKNOWN LOCATION: if you see this, please report it at https://github.com/avh4/elm-program-test/issues/>" t program state
|
||||
in
|
||||
advanceTimeIfSimulating 0
|
||||
>> Result.andThen (drainWorkQueue program)
|
||||
|
||||
|
||||
drainWorkQueue : Program model msg effect sub -> TestState model msg effect -> Result Failure (TestState model msg effect)
|
||||
drainWorkQueue program state =
|
||||
case state.effectSimulation of
|
||||
Nothing ->
|
||||
Ok state
|
||||
|
||||
Just simulation ->
|
||||
case EffectSimulation.stepWorkQueue simulation of
|
||||
Nothing ->
|
||||
-- work queue is empty
|
||||
Ok state
|
||||
|
||||
Just ( newSimulation, msg ) ->
|
||||
let
|
||||
updateMaybe tc =
|
||||
case msg of
|
||||
Nothing ->
|
||||
Ok tc
|
||||
|
||||
Just m ->
|
||||
update m program tc
|
||||
in
|
||||
{ state | effectSimulation = Just newSimulation }
|
||||
|> updateMaybe
|
||||
|> Result.andThen (drain program)
|
||||
|
||||
|
||||
advanceTime : String -> Int -> Program model msg effect sub -> TestState model msg effect -> Result Failure (TestState model msg effect)
|
||||
advanceTime functionName delta program state =
|
||||
case state.effectSimulation of
|
||||
Nothing ->
|
||||
Err (EffectSimulationNotConfigured functionName)
|
||||
|
||||
Just simulation ->
|
||||
advanceTo program functionName (simulation.state.nowMs + delta) state
|
||||
|
||||
|
||||
advanceTo : Program model msg effect sub -> String -> Int -> TestState model msg effect -> Result Failure (TestState model msg effect)
|
||||
advanceTo program functionName end state =
|
||||
case state.effectSimulation of
|
||||
Nothing ->
|
||||
Err (EffectSimulationNotConfigured functionName)
|
||||
|
||||
Just simulation ->
|
||||
let
|
||||
ss =
|
||||
simulation.state
|
||||
in
|
||||
case PairingHeap.findMin simulation.state.futureTasks of
|
||||
Nothing ->
|
||||
-- No future tasks to check
|
||||
Ok
|
||||
{ state
|
||||
| effectSimulation =
|
||||
Just
|
||||
{ simulation
|
||||
| state = { ss | nowMs = end }
|
||||
}
|
||||
}
|
||||
|
||||
Just ( t, task ) ->
|
||||
if t <= end then
|
||||
Ok
|
||||
{ state
|
||||
| effectSimulation =
|
||||
Just
|
||||
{ simulation
|
||||
| state =
|
||||
{ ss
|
||||
| nowMs = t
|
||||
, futureTasks = PairingHeap.deleteMin simulation.state.futureTasks
|
||||
}
|
||||
}
|
||||
}
|
||||
|> Result.map (withSimulation (EffectSimulation.queueTask (task ())))
|
||||
|> Result.andThen (drain program)
|
||||
|> Result.andThen (advanceTo program functionName end)
|
||||
|
||||
else
|
||||
-- next task is further in the future than we are advancing
|
||||
Ok
|
||||
{ state
|
||||
| effectSimulation =
|
||||
Just
|
||||
{ simulation
|
||||
| state = { ss | nowMs = end }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
withSimulation : (EffectSimulation msg effect -> EffectSimulation msg effect) -> TestState model msg effect -> TestState model msg effect
|
||||
withSimulation f state =
|
||||
{ state | effectSimulation = Maybe.map f state.effectSimulation }
|
31
examples/end-to-end/elm-program-test-src/Url/Extra.elm
Normal file
31
examples/end-to-end/elm-program-test-src/Url/Extra.elm
Normal file
@ -0,0 +1,31 @@
|
||||
module Url.Extra exposing (resolve)
|
||||
|
||||
{-| TODO: this module should implement the algorithm described at
|
||||
<https://url.spec.whatwg.org/>
|
||||
-}
|
||||
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
{-| This resolves a URL string (either an absolute or relative URL) against a base URL (given as a `Location`).
|
||||
-}
|
||||
resolve : Url -> String -> Url
|
||||
resolve base url =
|
||||
Url.fromString url
|
||||
-- TODO: implement correct logic (current logic is only correct for "authority-relative" URLs without query or fragment strings)
|
||||
|> Maybe.withDefault
|
||||
{ base
|
||||
| path =
|
||||
if String.startsWith "/" url then
|
||||
url
|
||||
|
||||
else
|
||||
String.split "/" base.path
|
||||
|> List.reverse
|
||||
|> List.drop 1
|
||||
|> List.reverse
|
||||
|> (\l -> l ++ String.split "/" url)
|
||||
|> String.join "/"
|
||||
, query = Nothing
|
||||
, fragment = Nothing
|
||||
}
|
@ -5,7 +5,8 @@
|
||||
"app",
|
||||
"../../src",
|
||||
".elm-pages",
|
||||
"../../plugins"
|
||||
"../../plugins",
|
||||
"elm-program-test-src"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
|
Loading…
Reference in New Issue
Block a user