Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions src/Navigation/Extra.elm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Navigation.Extra exposing (locationFromString)
module Navigation.Extra exposing (locationFromString, resolve)

{-| TODO: this module should implement the algorithm described at
<https://url.spec.whatwg.org/>
Expand All @@ -14,16 +14,28 @@ NOTE: the behavior of when `Nothing` is returned may change when the correct imp
-}
locationFromString : String -> Maybe Navigation.Location
locationFromString url =
Just
{ hash = "TODO"
, host = "TODO"
, hostname = "TODO"
, href = url
, origin = "TODO"
, password = "TODO"
, pathname = "/" ++ (url |> String.split "/" |> List.drop 3 |> String.join "/")
, port_ = "TODO"
, protocol = "TODO"
, search = "TODO"
, username = "TODO"
}
if String.contains "://" url then
Just
{ hash = "TODO"
, host = "TODO"
, hostname = "TODO"
, href = url
, origin = "TODO"
, password = "TODO"
, pathname = "/" ++ (url |> String.split "/" |> List.drop 3 |> String.join "/")
, port_ = "TODO"
, protocol = "TODO"
, search = "TODO"
, username = "TODO"
}
else
Nothing


{-| This resolves a URL string (either an absolute or relative URL) against a base URL (given as a `Location`).
-}
resolve : Navigation.Location -> String -> Navigation.Location
resolve base url =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, spooky - so it's okay that this allows people to get around the valid pathname constraint for now?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is actually how it should work (except for the unimplemented algorithm, of course).. this lets you call TestContext.routeChange in your test with a relative URL, and the relative URL you pass will be resolved against the current URL. So createWithNavigation still requires an absolute URL, but URLs given later will (hopefully) act the same as URLs in <a> tags that the user might click.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, got it! That seems right 👍

locationFromString url
-- TODO: implment correct logic (current logic is only correct for "authority-relative" URLs without query or fragment strings)
|> Maybe.withDefault { base | pathname = url }
160 changes: 128 additions & 32 deletions src/TestContext.elm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module TestContext
, clickButton
, create
, createWithFlags
, createWithJsonStringFlags
, createWithNavigation
, createWithNavigationAndFlags
, createWithNavigationAndJsonStringFlags
Expand All @@ -12,8 +13,10 @@ module TestContext
, expectModel
, expectView
, expectViewHas
, fail
, routeChange
, shouldHave
, shouldHaveLastEffect
, shouldHaveView
, shouldNotHave
, simulate
Expand All @@ -26,7 +29,7 @@ module TestContext
## Creating

@docs TestContext
@docs create, createWithFlags
@docs create, createWithFlags, createWithJsonStringFlags
@docs createWithNavigation, createWithNavigationAndFlags, createWithNavigationAndJsonStringFlags


Expand All @@ -43,14 +46,23 @@ module TestContext

## Final assertions

@docs expectViewHas, expectView, expectLastEffect, expectModel
@docs expectViewHas, expectView
@docs expectLastEffect, expectModel


## Intermediate assertions

These functions can be used to make assertions on a `TestContext` without ending the test.

@docs shouldHave, shouldNotHave, shouldHaveView
@docs shouldHaveLastEffect


## Custom assertions

These functions may be useful if you are writing your own custom assertion functions.

@docs fail

-}

Expand All @@ -68,7 +80,7 @@ import Test.Runner.Failure


type TestContext msg model effect
= TestContext (Result Failure ( TestProgram msg model effect, ( model, effect ) ))
= TestContext (Result Failure ( TestProgram msg model effect, ( model, effect ), Maybe Navigation.Location ))


type alias TestProgram msg model effect =
Expand All @@ -84,13 +96,16 @@ type Failure
| SimulateFailedToFindTarget String String
| InvalidLocationUrl String String
| InvalidFlags String String
| ProgramDoesNotSupportNavigation String
| CustomFailure String String


createHelper :
{ init : ( model, effect )
, update : msg -> model -> ( model, effect )
, view : model -> Html msg
, onRouteChange : Navigation.Location -> Maybe msg
, initialLocation : Maybe Navigation.Location
}
-> TestContext msg model effect
createHelper program =
Expand All @@ -101,6 +116,7 @@ createHelper program =
, onRouteChange = program.onRouteChange
}
, program.init
, program.initialLocation
)


Expand All @@ -116,6 +132,7 @@ create program =
, update = program.update
, view = program.view
, onRouteChange = \_ -> Nothing
, initialLocation = Nothing
}


Expand All @@ -132,9 +149,34 @@ createWithFlags program flags =
, update = program.update
, view = program.view
, onRouteChange = \_ -> Nothing
, initialLocation = Nothing
}


createWithJsonStringFlags :
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the options!!!

Json.Decode.Decoder flags
->
{ init : flags -> ( model, effect )
, update : msg -> model -> ( model, effect )
, view : model -> Html msg
}
-> String
-> TestContext msg model effect
createWithJsonStringFlags flagsDecoder program flagsJson =
case Json.Decode.decodeString flagsDecoder flagsJson of
Err message ->
TestContext <| Err (InvalidFlags "createWithJsonStringFlags" message)

Ok flags ->
createHelper
{ init = program.init flags
, update = program.update
, view = program.view
, onRouteChange = \_ -> Nothing
, initialLocation = Nothing
}


createWithNavigation :
(Navigation.Location -> msg)
->
Expand All @@ -155,6 +197,7 @@ createWithNavigation onRouteChange program initialUrl =
, update = program.update
, view = program.view
, onRouteChange = onRouteChange >> Just
, initialLocation = Just location
}


Expand All @@ -179,6 +222,7 @@ createWithNavigationAndFlags onRouteChange program initialUrl flags =
, update = program.update
, view = program.view
, onRouteChange = onRouteChange >> Just
, initialLocation = Just location
}


Expand Down Expand Up @@ -209,6 +253,7 @@ createWithNavigationAndJsonStringFlags flagsDecoder onRouteChange program initia
, update = program.update
, view = program.view
, onRouteChange = onRouteChange >> Just
, initialLocation = Just location
}


Expand All @@ -219,10 +264,11 @@ update msg (TestContext result) =
Err err ->
Err err

Ok ( program, ( model, _ ) ) ->
Ok ( program, ( model, _ ), currentLocation ) ->
Ok
( program
, program.update msg model
, currentLocation
)


Expand All @@ -232,7 +278,7 @@ simulateHelper functionDescription findTarget event (TestContext result) =
Err err ->
TestContext <| Err err

Ok ( program, ( model, _ ) ) ->
Ok ( program, ( model, _ ), _ ) ->
let
targetQuery =
program.view model
Expand Down Expand Up @@ -281,24 +327,27 @@ clickButton buttonText testContext =
testContext


{-| `url` may be an absolute URL or relative URL
-}
routeChange : String -> TestContext msg model effect -> TestContext msg model effect
routeChange url (TestContext result) =
case result of
Err err ->
TestContext <| Err err

Ok ( program, model ) ->
case Navigation.Extra.locationFromString url of
Nothing ->
TestContext <| Err (InvalidLocationUrl "routeChange" url)
Ok ( program, _, Nothing ) ->
TestContext <| Err (ProgramDoesNotSupportNavigation "routeChange")

Just location ->
case program.onRouteChange location of
Nothing ->
TestContext result
Ok ( program, _, Just currentLocation ) ->
case
Navigation.Extra.resolve currentLocation url
|> program.onRouteChange
of
Nothing ->
TestContext result

Just msg ->
update msg (TestContext result)
Just msg ->
update msg (TestContext result)


expectModel : (model -> Expectation) -> TestContext msg model effect -> Expectation
Expand All @@ -309,30 +358,41 @@ expectModel assertion (TestContext result) =
Err err ->
Err err

Ok ( program, ( model, lastEffect ) ) ->
Ok ( _, ( model, _ ), _ ) ->
case assertion model |> Test.Runner.getFailureReason of
Nothing ->
Ok ( program, ( model, lastEffect ) )
result

Just reason ->
Err (ExpectFailed "expectModel" reason.description reason.reason)


expectLastEffect : (effect -> Expectation) -> TestContext msg model effect -> Expectation
expectLastEffect assertion (TestContext result) =
done <|
TestContext <|
case result of
Err err ->
Err err
expectLastEffectHelper : String -> (effect -> Expectation) -> TestContext msg model effect -> TestContext msg model effect
expectLastEffectHelper functionName assertion (TestContext result) =
TestContext <|
case result of
Err err ->
Err err

Ok ( program, ( model, lastEffect ) ) ->
case assertion lastEffect |> Test.Runner.getFailureReason of
Nothing ->
Ok ( program, ( model, lastEffect ) )
Ok ( _, ( _, lastEffect ), _ ) ->
case assertion lastEffect |> Test.Runner.getFailureReason of
Nothing ->
result

Just reason ->
Err (ExpectFailed "expectLastEffect" reason.description reason.reason)
Just reason ->
Err (ExpectFailed functionName reason.description reason.reason)


shouldHaveLastEffect : (effect -> Expectation) -> TestContext msg model effect -> TestContext msg model effect
shouldHaveLastEffect assertion testContext =
expectLastEffectHelper "shouldHaveLastEffect" assertion testContext


expectLastEffect : (effect -> Expectation) -> TestContext msg model effect -> Expectation
expectLastEffect assertion testContext =
testContext
|> expectLastEffectHelper "expectLastEffect" assertion
|> done


expectViewHelper : String -> (Query.Single msg -> Expectation) -> TestContext msg model effect -> TestContext msg model effect
Expand All @@ -342,7 +402,7 @@ expectViewHelper functionName assertion (TestContext result) =
Err err ->
Err err

Ok ( program, ( model, lastEffect ) ) ->
Ok ( program, ( model, _ ), _ ) ->
case
model
|> program.view
Expand All @@ -351,7 +411,7 @@ expectViewHelper functionName assertion (TestContext result) =
|> Test.Runner.getFailureReason
of
Nothing ->
Ok ( program, ( model, lastEffect ) )
result

Just reason ->
Err (ExpectFailed functionName reason.description reason.reason)
Expand Down Expand Up @@ -406,3 +466,39 @@ done (TestContext result) =

Err (InvalidFlags functionName message) ->
Expect.fail (functionName ++ ":\n" ++ message)

Err (ProgramDoesNotSupportNavigation functionName) ->
Expect.fail (functionName ++ ": Program does not support navigation. Use TestContext.createWithNavigation or related function to create a TestContext that supports navigation.")

Err (CustomFailure assertionName message) ->
Expect.fail (assertionName ++ ": " ++ message)


{-| `fail` can be used to report custom errors if you are writing your own convenience functions to deal with test contexts.

Example (this is a function that checks for a particular structure in the program's view,
but will also fail the TestContext if the `expectedCount` parameter is invalid):

expectNotificationCount : Int -> TestContext Msg Model effect -> TestContext Msg Model effect
expectNotificationCount expectedCount testContext =
if expectedCount <= 0 then
testContext
|> TestContext.fail "expectNotificationCount"
("expectedCount must be positive, but was: " ++ toString expectedCount)
else
testContext
|> shouldHave
[ Test.Html.Selector.class "notifications"
, Test.Html.Selector.text (toString expectedCount)
]

-}
fail : String -> String -> TestContext msg model effect -> TestContext msg model effect
fail assertionName failureMessage (TestContext result) =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that you added this function!

TestContext <|
case result of
Err err ->
Err err

Ok _ ->
Err (CustomFailure assertionName failureMessage)
Loading