Skip to content

Instantly share code, notes, and snippets.

@avh4
Last active April 26, 2016 00:09
Show Gist options
  • Save avh4/7b732aa4ca5365d00157bdd80fa54841 to your computer and use it in GitHub Desktop.
Save avh4/7b732aa4ca5365d00157bdd80fa54841 to your computer and use it in GitHub Desktop.
module FakeSet (Set, member, empty, union, remove, insert) where
type alias Set a =
List a
member : a -> Set a -> Bool
member expected set =
List.any ((==) expected) set
empty : Set a
empty =
[]
union : Set a -> Set a -> Set a
union =
(++)
remove : a -> Set a -> Set a
remove x =
List.filter ((/=) x)
insert : a -> Set a -> Set a
insert =
(::)
module SyncableTests (..) where
import ElmTest exposing (..)
import FakeSet as Set exposing (Set)
import Json.Decode as Json
import Json.Encode
import Http
import Syncable exposing (..)
import String
-- HTTP convenience
getRequest : String -> Http.Request
getRequest url =
{ verb = "GET"
, url = url
, headers = []
, body = Http.empty
}
postRequest : String -> String -> Http.Request
postRequest url body =
{ verb = "POST"
, url = url
, headers = []
, body = Http.string body
}
-- Testing
type alias TestContext model transaction =
{ state : SyncState model transaction
, effects : Set Http.Request
, errors : List String
}
startForTest : Syncable model transaction -> TestContext model transaction
startForTest syncable =
{ state = fst <| init syncable
, effects = snd <| init syncable
, errors = []
}
assertHttpRequest : Http.Request -> TestContext model transaction -> Assertion
assertHttpRequest expected { effects } =
case Set.member expected effects of
True ->
pass
False ->
fail
("Expected an HTTP request to be made:"
++ "\n Expected: "
++ toString expected
++ "\n Actual: "
++ toString effects
)
stubResponse :
Http.Request
{ a : String
}
-> Result Http.Error String
-> TestContext model transaction
-> TestContext model transaction
stubResponse request response context =
case Set.member request context.effects of
True ->
{ context
| state = handleHttpResponse response context.state
, effects =
context.effects
|> Set.remove request
-- |> Set.union newEffects
}
False ->
{ context | errors = ("stubbed response was not made: " ++ toString request) :: context.errors }
currentModel : TestContext model transaction -> Result (List String) model
currentModel context =
case context.errors of
[] ->
Ok context.state.current
errors ->
Err errors
getSyncState : TestContext model transaction -> ExposedSyncState
getSyncState =
.state >> .syncState
applyUpdate : transaction -> TestContext model transaction -> TestContext model transaction
applyUpdate transaction context =
let
( newState, post ) =
handleLocalUpdate
transaction
context.state
in
{ context
| state = newState
, effects = Set.insert post context.effects
}
applyUpdates : List transaction -> TestContext model transaction -> TestContext model transaction
applyUpdates transactions context =
List.foldl applyUpdate context transactions
-- all' : Test
-- all' =
-- describe "Syncable"
-- (makeIt "" (++) Json.string Json.Encode.string "server")
-- [ describe "initial request"
-- (startForTest)
-- [ with "successful response"
-- ( stubResponse
-- (getRequest "server/")
-- (Ok """{"id":"f00", "transactions":["A", "B", "C"]}""")
-- )
-- ]
-- ]
all : Test
all =
let
assertCurrentModel expected context =
case currentModel context of
Ok actual ->
assertEqual expected actual
Err errors ->
fail
<| String.join "\n "
<| [ "error occurred:", "ERRORS:" ]
++ errors
++ [ "PENDING EFFECTS:" ]
++ (List.map toString context.effects)
testApp =
makeIt "" (flip (++)) Json.string Json.Encode.string "server"
|> startForTest
afterInitialLoad =
testApp
|> stubResponse
(getRequest "server/")
(Ok """{"id":"f00", "transactions":[]}""")
in
suite
"Syncable"
[ testApp
|> applyUpdates [ "X", "Y" ]
|> assertCurrentModel "XY"
|> test "applies local transactions"
, testApp
|> stubResponse
(getRequest "server/")
(Ok """{}{"id":"f00", "transactions":["A", "B", "C"]}""")
(Ok """{"id":"f00", "transactions":["A", "B", "C"]}""")
|> assertCurrentModel "ABC"
|> test "applies initial transactions from the server"
, afterInitialLoad
|> applyUpdates [ "X" ]
|> assertHttpRequest (postRequest "server/txns" """{"base":"f00","transactions":["X"]}""")
|> test "sends local transactions to server"
, testApp
|> stubResponse
(getRequest "server/")
(Ok """{"id":"f00", "transactions":["A", "B", "C"]}""")
|> applyUpdates [ "X", "Y" ]
|> assertCurrentModel "ABCXY"
|> test "after initial load completes, applies local transactions"
, testApp
|> applyUpdates [ "X", "Y" ]
|> stubResponse
(getRequest "server/")
(Ok """{"id":"f00", "transactions":["A", "B", "C"]}""")
|> assertCurrentModel "ABCXY"
|> test "when initial load completes, previous local transactions are replayed"
, afterInitialLoad
|> applyUpdates [ "X" ]
|> stubResponse
(postRequest "server/txns" """{"base":"f00","transactions":["X"]}""")
(Ok """{"id":"f0x", "transactions":["X"]}""")
|> assertCurrentModel "X"
|> test "getting server confirmation of a local change"
, afterInitialLoad
|> applyUpdates [ "X", "Y", "Z" ]
|> stubResponse
(postRequest "server/txns" """{"base":"f00","transactions":["X"]}""")
(Ok """{"id":"f0x", "transactions":["X"]}""")
|> assertCurrentModel "XYZ"
|> test "getting server confirmation of a local change with more pending changes"
, afterInitialLoad
|> applyUpdates [ "W", "X", "Y", "Z" ]
|> stubResponse
(postRequest "server/txns" """{"base":"f00","transactions":["W","X"]}""")
(Ok """{"id":"f0w", "transactions":["W","X"]}""")
|> assertCurrentModel "WXYZ"
|> test "getting server confirmation for second of multiple transactions"
, afterInitialLoad
|> applyUpdates [ "W", "X", "Y", "Z" ]
|> stubResponse
(postRequest "server/txns" """{"base":"f00","transactions":["W"]}""")
(Ok """{"id":"f0w", "transactions":["W"]}""")
|> stubResponse
(postRequest "server/txns" """{"base":"f00","transactions":["W","X"]}""")
(Ok """{"id":"f0w", "transactions":["W","X"]}""")
|> assertCurrentModel "WXYZ"
|> test "getting server confirmation for multiple transactions"
, afterInitialLoad
|> applyUpdates [ "W", "X", "Y", "Z" ]
|> stubResponse
(postRequest "server/txns" """{"base":"f0x","transactions":["Y"]}""")
(Ok """{"id":"f0y", "transactions":["Y"]}""")
|> stubResponse
(postRequest "server/txns" """{"base":"f0y","transactions":["Z"]}""")
(Ok """{"id":"f0z", "transactions":["Z"]}""")
|> stubResponse
(postRequest "server/txns" """{"base":"f0w","transactions":["X"]}""")
(Ok """{"id":"f0x", "transactions":["X"]}""")
|> stubResponse
(postRequest "server/txns" """{"base":"f00","transactions":["W"]}""")
(Ok """{"id":"f0w", "transactions":["W"]}""")
|> assertCurrentModel "WXYZ"
|> test "getting server confirmation out of order"
, afterInitialLoad
|> applyUpdates [ "X", "X" ]
|> stubResponse
(postRequest "server/txns" """{"base":"f00","transactions":["X"]}""")
(Ok """{"id":"f0x", "transactions":["X"]}""")
|> assertCurrentModel "XX"
|> test "getting server confirmation for one of multiple equivalent transactions"
-- What is a real use case of having equivalent transactions that need to be distinct? Wouldn't a transaction value have either a generated id or a timestamp in a real data model?
, let
assertSyncState expected =
getSyncState >> assertEqual expected
in
suite
"sync state"
[ testApp
|> assertSyncState Connecting
|> test "initial state is connecting"
, testApp
|> stubResponse
(getRequest "server/")
(Ok """{"id":"f00", "transactions":[]}""")
|> assertSyncState Active
|> test "is active after successful initial load"
, testApp
|> stubResponse
(getRequest "server/")
(Err Http.NetworkError)
|> assertSyncState (HttpError Http.NetworkError)
|> test "when initial request fails (HTTP), shows not connected"
, testApp
|> stubResponse
(getRequest "server/")
(Ok "not json")
|> assertSyncState (JsonError "Unexpected token o")
|> test "when initial request fails (JSON), shows not connected"
]
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment