From 6f4b1812fd24f0233980e0d72a95b8569e370dc9 Mon Sep 17 00:00:00 2001 From: Aaron VonderHaar Date: Thu, 22 Mar 2018 16:48:36 -0700 Subject: [PATCH 1/4] TestContext.routeChange works with (some) relative URLs --- src/Navigation/Extra.elm | 40 ++++++++++++++++++---------- src/TestContext.elm | 53 ++++++++++++++++++++++++-------------- tests/TestContextTests.elm | 11 ++++++++ 3 files changed, 71 insertions(+), 33 deletions(-) diff --git a/src/Navigation/Extra.elm b/src/Navigation/Extra.elm index f53c215..4857c227 100644 --- a/src/Navigation/Extra.elm +++ b/src/Navigation/Extra.elm @@ -1,4 +1,4 @@ -module Navigation.Extra exposing (locationFromString) +module Navigation.Extra exposing (locationFromString, resolve) {-| TODO: this module should implement the algorithm described at @@ -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 = + 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 } diff --git a/src/TestContext.elm b/src/TestContext.elm index 99ed755..380117a 100644 --- a/src/TestContext.elm +++ b/src/TestContext.elm @@ -68,7 +68,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 = @@ -84,6 +84,7 @@ type Failure | SimulateFailedToFindTarget String String | InvalidLocationUrl String String | InvalidFlags String String + | ProgramDoesNotSupportNavigation String createHelper : @@ -91,6 +92,7 @@ createHelper : , update : msg -> model -> ( model, effect ) , view : model -> Html msg , onRouteChange : Navigation.Location -> Maybe msg + , initialLocation : Maybe Navigation.Location } -> TestContext msg model effect createHelper program = @@ -101,6 +103,7 @@ createHelper program = , onRouteChange = program.onRouteChange } , program.init + , program.initialLocation ) @@ -116,6 +119,7 @@ create program = , update = program.update , view = program.view , onRouteChange = \_ -> Nothing + , initialLocation = Nothing } @@ -132,6 +136,7 @@ createWithFlags program flags = , update = program.update , view = program.view , onRouteChange = \_ -> Nothing + , initialLocation = Nothing } @@ -155,6 +160,7 @@ createWithNavigation onRouteChange program initialUrl = , update = program.update , view = program.view , onRouteChange = onRouteChange >> Just + , initialLocation = Just location } @@ -179,6 +185,7 @@ createWithNavigationAndFlags onRouteChange program initialUrl flags = , update = program.update , view = program.view , onRouteChange = onRouteChange >> Just + , initialLocation = Just location } @@ -209,6 +216,7 @@ createWithNavigationAndJsonStringFlags flagsDecoder onRouteChange program initia , update = program.update , view = program.view , onRouteChange = onRouteChange >> Just + , initialLocation = Just location } @@ -219,10 +227,11 @@ update msg (TestContext result) = Err err -> Err err - Ok ( program, ( model, _ ) ) -> + Ok ( program, ( model, _ ), currentLocation ) -> Ok ( program , program.update msg model + , currentLocation ) @@ -232,7 +241,7 @@ simulateHelper functionDescription findTarget event (TestContext result) = Err err -> TestContext <| Err err - Ok ( program, ( model, _ ) ) -> + Ok ( program, ( model, _ ), _ ) -> let targetQuery = program.view model @@ -281,24 +290,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 @@ -309,10 +321,10 @@ 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) @@ -326,10 +338,10 @@ expectLastEffect assertion (TestContext result) = Err err -> Err err - Ok ( program, ( model, lastEffect ) ) -> + Ok ( _, ( _, lastEffect ), _ ) -> case assertion lastEffect |> Test.Runner.getFailureReason of Nothing -> - Ok ( program, ( model, lastEffect ) ) + result Just reason -> Err (ExpectFailed "expectLastEffect" reason.description reason.reason) @@ -342,7 +354,7 @@ expectViewHelper functionName assertion (TestContext result) = Err err -> Err err - Ok ( program, ( model, lastEffect ) ) -> + Ok ( program, ( model, _ ), _ ) -> case model |> program.view @@ -351,7 +363,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) @@ -406,3 +418,6 @@ 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.") diff --git a/tests/TestContextTests.elm b/tests/TestContextTests.elm index 87378e7..c55b447 100644 --- a/tests/TestContextTests.elm +++ b/tests/TestContextTests.elm @@ -95,6 +95,17 @@ all = "https://example.com/path" |> TestContext.routeChange "https://example.com/new" |> TestContext.expectModel (Expect.equal ";/new") + , test "can simulate a route change with a relative URL" <| + \() -> + TestContext.createWithNavigation + .pathname + { init = \location -> ( "", NoOp ) + , update = testUpdate + , view = testView + } + "https://example.com/path" + |> TestContext.routeChange "/new" + |> TestContext.expectModel (Expect.equal ";/new") , test "can create with navigation and flags" <| \() -> TestContext.createWithNavigationAndFlags From 9b0da6680fb84d6487bebe296f4d797a019a14cf Mon Sep 17 00:00:00 2001 From: Aaron VonderHaar Date: Thu, 22 Mar 2018 17:15:13 -0700 Subject: [PATCH 2/4] TestContext.createWithJsonStringFlags --- src/TestContext.elm | 27 ++++++++++++++++++++++++++- tests/TestContextTests.elm | 10 ++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/TestContext.elm b/src/TestContext.elm index 380117a..50c0d1f 100644 --- a/src/TestContext.elm +++ b/src/TestContext.elm @@ -4,6 +4,7 @@ module TestContext , clickButton , create , createWithFlags + , createWithJsonStringFlags , createWithNavigation , createWithNavigationAndFlags , createWithNavigationAndJsonStringFlags @@ -26,7 +27,7 @@ module TestContext ## Creating @docs TestContext -@docs create, createWithFlags +@docs create, createWithFlags, createWithJsonStringFlags @docs createWithNavigation, createWithNavigationAndFlags, createWithNavigationAndJsonStringFlags @@ -140,6 +141,30 @@ createWithFlags program flags = } +createWithJsonStringFlags : + 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) -> diff --git a/tests/TestContextTests.elm b/tests/TestContextTests.elm index c55b447..3d2a0f1 100644 --- a/tests/TestContextTests.elm +++ b/tests/TestContextTests.elm @@ -74,6 +74,16 @@ all = } "flags" |> TestContext.expectModel (Expect.equal "") + , test "can create with JSON string flags" <| + \() -> + TestContext.createWithJsonStringFlags + (Json.Decode.field "y" Json.Decode.string) + { init = \flags -> ( "", NoOp ) + , update = testUpdate + , view = testView + } + """{"y": "fromJson"}""" + |> TestContext.expectModel (Expect.equal "") , test "can create with navigation" <| \() -> TestContext.createWithNavigation From 83f0ca9f0b2cf4ed3aeaaea0eb3b37cfd9c2c893 Mon Sep 17 00:00:00 2001 From: Aaron VonderHaar Date: Thu, 22 Mar 2018 17:47:41 -0700 Subject: [PATCH 3/4] TestContext.fail --- src/TestContext.elm | 42 ++++++++++++++++++++++++++++++++++++++ tests/TestContextTests.elm | 9 ++++++++ 2 files changed, 51 insertions(+) diff --git a/src/TestContext.elm b/src/TestContext.elm index 50c0d1f..a3879c4 100644 --- a/src/TestContext.elm +++ b/src/TestContext.elm @@ -13,6 +13,7 @@ module TestContext , expectModel , expectView , expectViewHas + , fail , routeChange , shouldHave , shouldHaveView @@ -53,6 +54,13 @@ These functions can be used to make assertions on a `TestContext` without ending @docs shouldHave, shouldNotHave, shouldHaveView + +## Custom assertions + +These functions may be useful if you are writing your own custom assertion functions. + +@docs fail + -} import Expect exposing (Expectation) @@ -86,6 +94,7 @@ type Failure | InvalidLocationUrl String String | InvalidFlags String String | ProgramDoesNotSupportNavigation String + | CustomFailure String String createHelper : @@ -446,3 +455,36 @@ done (TestContext result) = 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) = + TestContext <| + case result of + Err err -> + Err err + + Ok _ -> + Err (CustomFailure assertionName failureMessage) diff --git a/tests/TestContextTests.elm b/tests/TestContextTests.elm index 3d2a0f1..49e5c05 100644 --- a/tests/TestContextTests.elm +++ b/tests/TestContextTests.elm @@ -8,6 +8,7 @@ import Json.Encode import Test exposing (..) import Test.Html.Query as Query import Test.Html.Selector as Selector +import Test.Runner import TestContext exposing (TestContext) @@ -180,4 +181,12 @@ all = testContext |> TestContext.clickButton "Click Me" |> TestContext.expectLastEffect (Expect.equal (LogUpdate "CLICK")) + , test "can be forced into failure" <| + \() -> + testContext + |> TestContext.fail "custom" "Because I said so" + |> TestContext.done + |> Test.Runner.getFailureReason + |> Maybe.map .description + |> Expect.equal (Just "custom: Because I said so") ] From 8bbd333763eae1160560b14c41ed4581fd6f8b75 Mon Sep 17 00:00:00 2001 From: Aaron VonderHaar Date: Thu, 22 Mar 2018 18:15:07 -0700 Subject: [PATCH 4/4] TestContext.shouldHaveLastEffect --- src/TestContext.elm | 42 +++++++++++++++++++++++++------------- tests/TestContextTests.elm | 7 +++++++ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/TestContext.elm b/src/TestContext.elm index a3879c4..3d7ba27 100644 --- a/src/TestContext.elm +++ b/src/TestContext.elm @@ -16,6 +16,7 @@ module TestContext , fail , routeChange , shouldHave + , shouldHaveLastEffect , shouldHaveView , shouldNotHave , simulate @@ -45,7 +46,8 @@ module TestContext ## Final assertions -@docs expectViewHas, expectView, expectLastEffect, expectModel +@docs expectViewHas, expectView +@docs expectLastEffect, expectModel ## Intermediate assertions @@ -53,6 +55,7 @@ module TestContext These functions can be used to make assertions on a `TestContext` without ending the test. @docs shouldHave, shouldNotHave, shouldHaveView +@docs shouldHaveLastEffect ## Custom assertions @@ -364,21 +367,32 @@ expectModel assertion (TestContext result) = 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 ( _, ( _, lastEffect ), _ ) -> - case assertion lastEffect |> Test.Runner.getFailureReason of - Nothing -> - result + 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 diff --git a/tests/TestContextTests.elm b/tests/TestContextTests.elm index 49e5c05..0dcfe71 100644 --- a/tests/TestContextTests.elm +++ b/tests/TestContextTests.elm @@ -181,6 +181,13 @@ all = testContext |> TestContext.clickButton "Click Me" |> TestContext.expectLastEffect (Expect.equal (LogUpdate "CLICK")) + , test "can assert on the last effect as an intermediate assertion" <| + \() -> + testContext + |> TestContext.shouldHaveLastEffect (Expect.equal NoOp) + |> TestContext.clickButton "Click Me" + |> TestContext.shouldHaveLastEffect (Expect.equal (LogUpdate "CLICK")) + |> TestContext.done , test "can be forced into failure" <| \() -> testContext