datastar-hs-0.1.0.2: test/Hypermedia/Datastar/SSESpec.hs
module Hypermedia.Datastar.SSESpec where
import Data.Text qualified as T
import Test.Hspec
import Data.ByteString.Builder qualified as BSB
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.IORef
import Data.List (isInfixOf)
import Hypermedia.Datastar
import Hypermedia.Datastar.Logger (nullLogger)
import Hypermedia.Datastar.PatchElements
import Hypermedia.Datastar.PatchSignals
import Hypermedia.Datastar.Types
import Hypermedia.Datastar.WAI (renderEvent)
import Network.Wai.Internal (Response (..))
render :: DatastarEvent -> String
render = LBS.unpack . BSB.toLazyByteString . renderEvent
-- | Consume a streaming WAI response and return the full body as a string.
consumeStreamingResponse :: Response -> IO String
consumeStreamingResponse (ResponseStream _status _headers body) = do
chunksRef <- newIORef []
body
(\chunk -> modifyIORef' chunksRef (chunk :))
(pure ())
chunks <- readIORef chunksRef
pure $ LBS.unpack $ BSB.toLazyByteString $ mconcat $ reverse chunks
consumeStreamingResponse _ = error "Expected a streaming response"
spec :: Spec
spec = do
describe "Hypermedia.Datastar.SSE.renderEvent" $ do
it "renders a simple event" $ do
let event =
DatastarEvent
{ eventType = EventPatchElements
, eventId = Nothing
, retry = defaultRetryDuration
, dataLines = ["elements <div id=\"feed\"><span>1</span></div>"]
}
render event
`shouldBe` "event: datastar-patch-elements\n\
\data: elements <div id=\"feed\"><span>1</span></div>\n\
\\n"
describe "SSE streaming" $ do
it "sends multiple events over a single SSE connection" $ do
let e0 = "<div id=\"a\">1</div>"
e1 = "<div id=\"b\">2</div>"
e2 = "{\"count\": 42}"
resp = sseResponse nullLogger $ \gen -> do
sendPatchElements gen (patchElements e0)
sendPatchElements gen (patchElements e1)
sendPatchSignals gen (patchSignals e2)
body <- consumeStreamingResponse resp
let s0 = "elements " ++ T.unpack e0
s1 = "elements " ++ T.unpack e1
s2 = "signals " ++ T.unpack e2
-- All three events are present in the single response body
length (filter ("event: datastar-patch-elements" `isInfixOf`) (lines body))
`shouldBe` 2
length (filter ("event: datastar-patch-signals" `isInfixOf`) (lines body))
`shouldBe` 1
-- The actual data is there too
body `shouldSatisfy` isInfixOf s0
body `shouldSatisfy` isInfixOf s1
body `shouldSatisfy` isInfixOf s2
-- Events arrive in the order they were sent
let eventLines = filter (\l -> "event: " `isInfixOf` l || "data: " `isInfixOf` l) (lines body)
indexOf needle = length $ takeWhile (not . isInfixOf needle) eventLines
indexOf s0 `shouldSatisfy` (< indexOf s1)
indexOf s1 `shouldSatisfy` (< indexOf s2)