packages feed

datastar-hs-1.1.0.0: 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.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)