stache-2.4.0: tests/Text/Mustache/RenderSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Text.Mustache.RenderSpec
( main,
spec,
)
where
import Data.Aeson (KeyValue (..), Value (..), object)
import Data.Map qualified as M
import Data.Text (Text)
import Test.Hspec
import Text.Megaparsec
import Text.Mustache.Render
import Text.Mustache.Type
main :: IO ()
main = hspec spec
spec :: Spec
spec = describe "renderMustache" $ do
let w ns value =
let template = Template "test" (M.singleton "test" ns)
in fst (renderMustacheW template value)
r ns value =
let template = Template "test" (M.singleton "test" ns)
in renderMustache template value
key = Key . pure
it "leaves text block “as is”" $
r [TextBlock "a text block"] Null `shouldBe` "a text block"
it "renders escaped variables correctly" $
r
[EscapedVar (key "foo")]
(object ["foo" .= ("<html>&\"'something'\"</html>" :: Text)])
`shouldBe` "<html>&"'something'"</html>"
it "renders unescaped variables “as is”" $
r
[UnescapedVar (key "foo")]
(object ["foo" .= ("<html>&\"something\"</html>" :: Text)])
`shouldBe` "<html>&\"something\"</html>"
context "when rendering a variable" $ do
context "when variable does not exist" $
it "generates correct warning" $
w [EscapedVar (key "foo")] (object [])
`shouldBe` [MustacheVariableNotFound (key "foo")]
context "when variable is not non-scalar" $
it "generates correct warning" $
w [EscapedVar (key "foo")] (object ["foo" .= object []])
`shouldBe` [MustacheDirectlyRenderedValue (key "foo")]
context "when rendering a section" $ do
let nodes = [Section (key "foo") [UnescapedVar (key "bar"), TextBlock "*"]]
context "when the key is not present" $ do
it "renders nothing" $
r nodes (object []) `shouldBe` ""
it "generates correct warning" $
w nodes (object []) `shouldBe` [MustacheVariableNotFound (key "foo")]
context "when the key is present" $ do
context "when the key is a “false” value" $ do
it "skips the Null value" $
r nodes (object ["foo" .= Null]) `shouldBe` ""
it "skips false Boolean" $
r nodes (object ["foo" .= False]) `shouldBe` ""
it "skips empty list" $
r nodes (object ["foo" .= ([] :: [Text])]) `shouldBe` ""
it "skips empty object" $
r nodes (object ["foo" .= object []]) `shouldBe` ""
it "skips empty string" $
r nodes (object ["foo" .= ("" :: Text)]) `shouldBe` ""
context "when the key is a Boolean true" $
it "renders the section without interpolation" $
r
[Section (key "foo") [TextBlock "brr"]]
(object ["foo" .= object ["bar" .= True]])
`shouldBe` "brr"
context "when the key is an object" $
it "uses it to render section once" $
r nodes (object ["foo" .= object ["bar" .= ("huh?" :: Text)]])
`shouldBe` "huh?*"
context "when the key is a singleton list" $
it "uses it to render section once" $
r nodes (object ["foo" .= object ["bar" .= ("huh!" :: Text)]])
`shouldBe` "huh!*"
context "when the key is a list of Boolean trues" $
it "renders the section as many times as there are elements" $
r
[Section (key "foo") [TextBlock "brr"]]
(object ["foo" .= [True, True]])
`shouldBe` "brrbrr"
context "when the key is a list of objects" $
it "renders the section many times changing context" $
r nodes (object ["foo" .= [object ["bar" .= x] | x <- [1 .. 4] :: [Int]]])
`shouldBe` "1*2*3*4*"
context "when the key is a number" $ do
it "renders the section" $
r
[Section (key "foo") [TextBlock "brr"]]
(object ["foo" .= (5 :: Int)])
`shouldBe` "brr"
it "uses the key as context" $
r
[Section (key "foo") [EscapedVar (Key [])]]
(object ["foo" .= (5 :: Int)])
`shouldBe` "5"
context "when the key is a non-empty string" $ do
it "renders the section" $
r
[Section (key "foo") [TextBlock "brr"]]
(object ["foo" .= ("x" :: Text)])
`shouldBe` "brr"
it "uses the key as context" $
r
[Section (key "foo") [EscapedVar (Key [])]]
(object ["foo" .= ("x" :: Text)])
`shouldBe` "x"
context "when rendering an inverted section" $ do
let nodes = [InvertedSection (key "foo") [TextBlock "Here!"]]
context "when the key is not present" $ do
it "renders the inverse section" $
r nodes (object []) `shouldBe` "Here!"
it "generates correct warning" $
w nodes (object []) `shouldBe` [MustacheVariableNotFound (key "foo")]
context "when the key is present" $ do
context "when the key is a “false” value" $ do
it "renders with Null value" $
r nodes (object ["foo" .= Null]) `shouldBe` "Here!"
it "renders with false Boolean" $
r nodes (object ["foo" .= False]) `shouldBe` "Here!"
it "renders with empty list" $
r nodes (object ["foo" .= ([] :: [Text])]) `shouldBe` "Here!"
it "renders with empty object" $
r nodes (object ["foo" .= object []]) `shouldBe` "Here!"
context "when the key is a “true” value" $ do
it "skips true Boolean" $
r nodes (object ["foo" .= True]) `shouldBe` ""
it "skips non-empty object" $
r nodes (object ["foo" .= object ["bar" .= True]]) `shouldBe` ""
it "skips non-empty list" $
r nodes (object ["foo" .= [True]]) `shouldBe` ""
context "when rendering a partial" $ do
let nodes =
[ Partial "partial" (Just $ mkPos 4),
TextBlock "*"
]
it "skips missing partial" $
r nodes Null `shouldBe` " *"
it "renders partial correctly" $
let template =
Template "test" $
M.fromList
[ ("test", nodes),
("partial", [TextBlock "one\ntwo\nthree"])
]
in renderMustache template Null
`shouldBe` " one\n two\n three*"
context "when rendering a partial with list sections" $ do
it "maintains indentation for all iterations in a partial with sections" $
let template =
Template "test" $
M.fromList
[ ("test", [TextBlock "Subnets:\n", Partial "myPartial" (Just $ mkPos 3)]),
( "myPartial",
[ Section
(key "subnets")
[ TextBlock "- ",
EscapedVar (Key []),
TextBlock "\n\"Test string\"\n"
]
]
)
]
value =
object
[ "subnets"
.= [ "subnet-0a0a0a0a" :: Text,
"subnet-0b0b0b0b",
"subnet-0c0c0c0c"
]
]
in renderMustache template value
`shouldBe` "Subnets:\n - subnet-0a0a0a0a\n \"Test string\"\n - subnet-0b0b0b0b\n \"Test string\"\n - subnet-0c0c0c0c\n \"Test string\"\n"
context "when rendering nested partials with list sections" $ do
it "maintains indentation through multiple levels of nested partials with sections" $
let template =
Template "test" $
M.fromList
[ ("test", [TextBlock "Configuration:\n", Partial "level1" (Just $ mkPos 3)]),
( "level1",
[ TextBlock "Services:\n",
Partial "level2" (Just $ mkPos 3)
]
),
( "level2",
[ Section
(key "services")
[ TextBlock "- name: ",
EscapedVar (key "name"),
TextBlock "\n port: ",
EscapedVar (key "port"),
TextBlock "\n"
]
]
)
]
value =
object
[ "services"
.= [ object ["name" .= ("web" :: Text), "port" .= (8080 :: Int)],
object ["name" .= ("api" :: Text), "port" .= (3000 :: Int)],
object ["name" .= ("db" :: Text), "port" .= (5432 :: Int)]
]
]
in renderMustache template value
`shouldBe` "Configuration:\n Services:\n - name: web\n port: 8080\n - name: api\n port: 3000\n - name: db\n port: 5432\n"
it "handles mixed content with nested partials and multiple sections" $
let template =
Template "test" $
M.fromList
[ ( "test",
[ TextBlock "Project:\n",
Section
(key "project")
[ TextBlock " Name: ",
EscapedVar (key "name"),
TextBlock "\n",
Partial "components" (Just $ mkPos 3)
]
]
),
( "components",
[ TextBlock "Components:\n",
Section
(key "components")
[ Partial "component-detail" (Just $ mkPos 3)
]
]
),
( "component-detail",
[ TextBlock "- ",
EscapedVar (key "type"),
TextBlock ":\n",
Section
(key "items")
[ TextBlock " * ",
EscapedVar (Key []),
TextBlock "\n"
]
]
)
]
value =
object
[ "project"
.= [ object
[ "name" .= ("MyApp" :: Text),
"components"
.= [ object
[ "type" .= ("Frontend" :: Text),
"items" .= ["React" :: Text, "TypeScript", "CSS"]
],
object
[ "type" .= ("Backend" :: Text),
"items" .= ["Node.js" :: Text, "Express", "MongoDB"]
]
]
]
]
]
in renderMustache template value
`shouldBe` "Project:\n Name: MyApp\n Components:\n - Frontend:\n * React\n * TypeScript\n * CSS\n - Backend:\n * Node.js\n * Express\n * MongoDB\n"
context "when rendering a nested partial" $
it "renders outer partial correctly" $
let template =
Template "outer" $
M.fromList
[ ("inner", [TextBlock "x"]),
("middle", [Partial "inner" (Just $ mkPos 1)]),
("outer", [Partial "middle" (Just $ mkPos 1)])
]
in renderMustache template Null `shouldBe` "x"
context "when using dotted keys inside a section" $
it "it should be equivalent to access via one more section" $
r
[ Section
(key "things")
[ EscapedVar (Key ["atts", "color"]),
TextBlock " == ",
Section (key "atts") [EscapedVar (key "color")]
]
]
( object
[ "things"
.= [ object
[ "atts"
.= object ["color" .= ("blue" :: Text)]
]
]
]
)
`shouldBe` "blue == blue"