ginger-0.3.7.0: test/Text/Ginger/SimulationTests.hs
{-#LANGUAGE OverloadedStrings #-}
{-#LANGUAGE FlexibleContexts #-}
module Text.Ginger.SimulationTests
where
import Text.Ginger
import Text.Ginger.Html
import Test.Tasty
import Test.Tasty.HUnit
import Data.Default (def)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Maybe (fromMaybe)
import Control.Monad.Writer (WriterT, runWriterT)
import Control.Monad.IO.Class (liftIO)
import Data.IORef
import Data.Monoid
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy.UTF8 as LUTF8
import qualified Data.ByteString.Lazy as LBS
import Data.Time (TimeLocale)
simulationTests :: TestTree
simulationTests = testGroup "Simulation"
[ testCase "Smoke Test" $ mkTestHtml [] [] "Hello" "Hello"
, testGroup "Comments"
-- There is a comment between the two dashes that should not appear in
-- the output.
[ testCase "Comment does not appear in output" $ mkTestHtml
[] [] "- {# Comments #} -" "- -"
]
, testGroup "Dashed limiters eat whitespace"
[ testCase "comments" $ mkTestHtml
[] [] "- {#- Comment -#} -" "--"
, testCase "interpolations" $ mkTestHtml
[] [] "- {{- '' -}} -" "--"
, testCase "flow" $ mkTestHtml
[] [] "- {%- set x=1 -%} -" "--"
]
, testGroup "Literals"
[ testCase "String: \"foobar\"" $ mkTestHtml
[] [] "{{ \"foobar\" }}" "foobar"
, testGroup "Numbers"
[ testCase "123" $ mkTestHtml
[] [] "{{ 123 }}" "123"
, testCase "3.1415" $ mkTestHtml
[] [] "{{ 3.1415 }}" "3.1415"
]
, testGroup "Booleans"
[ testCase "true" $ mkTestHtml
[] [] "{{ true }}" "1"
, testCase "false" $ mkTestHtml
[] [] "{{ false }}" ""
]
, testCase "Null" $ mkTestHtml
[] [] "{{ null }}" ""
]
, testGroup "Simple list/object constructs"
[ testCase "Lists" $ mkTestHtml
[] [] "{{ [\"foo\",\"bar\",\"baz\" ] }}" "foobarbaz"
, testCase "Nested lists" $ mkTestHtml
[] [] "{{ [ \"foo\", \"bar\", [ 1, 2, 3 ], \"baz\" ] }}" "foobar123baz"
, testCase "Objects" $ mkTestHtml
[] [] "{{ { \"foo\":\"bar\" } }}" "bar"
, testCase "Nested object/list constructs" $ mkTestHtml
[] [] "{{ { \"foo\":[\"foo\", {\"asdf\" : \"bar\"}, [\"baz\" ]] } }}" "foobarbaz"
]
, testGroup "Accessing object/list members"
[ testCase "by integer index" $ mkTestHtml
[] [] "{{ [ \"foo\", \"bar\" ][1] }}" "bar"
, testCase "by string key" $ mkTestHtml
[] [] "{{ { \"foo\": \"bar\", \"baz\": \"quux\" }['foo'] }}" "bar"
, testCase "by property name" $ mkTestHtml
[] [] "{{ { \"foo\": \"bar\", \"baz\": \"quux\" }.foo }}" "bar"
, testCase "multi-level mixed" $ mkTestHtml
[] [] "{{ { \"foo\": { \"oink\": \"nope\", \"baz\": { \"boop\": [], \"quux\": \"bar\" }}}.foo.baz[\"quux\"] }}" "bar"
]
, testGroup "Function calls"
-- In order to make sure the correct function is called, we write one
-- that stores its argument in an IORef that it closes over, thus
-- making it impossible for other functions to modify it; then we
-- assert that the IORef contains what we expect.
[ testCase "print(\"Hello\")" $ do
buf <- newIORef ""
let printF :: Function (Run IO Html)
printF = \xs -> do
liftIO . writeIORef buf . mconcat . map (asText . snd) $ xs
return def
mkTestHtml [("print", fromFunction printF)] [] "{{ print(\"Hello\") }}" ""
actual <- readIORef buf
let expected = "Hello"
assertEqual "" actual expected
, testCase "\"Hello\"|print" $ do
buf <- newIORef ""
let printF :: Function (Run IO Html)
printF = \xs -> do
liftIO . writeIORef buf . mconcat . map (asText . snd) $ xs
return def
mkTestHtml [("print", fromFunction printF)] [] "{{ \"Hello\"|print }}" ""
actual <- readIORef buf
let expected = "Hello"
assertEqual "" actual expected
]
, testGroup "Addition"
[ testCase "1 + 1 = 2" $ do
mkTestHtml [] [] "{{ sum(1, 1) }}" "2"
, testCase "1 + 1 = 2" $ do
mkTestHtml [] [] "{{ 1 + 1 }}" "2"
]
, testGroup "Subtraction"
[ testCase "1 - 1 = 0" $ do
mkTestHtml [] [] "{{ 1 - 1 }}" "0"
]
, testGroup "Concatenation"
[ testCase "1 ~ \"foo\" = 1foo" $ do
mkTestHtml [] [] "{{ 1 ~ \"foo\" }}" "1foo"
]
, testGroup "Multiplication"
[ testCase "5 * 5 = 25" $ do
mkTestHtml [] [] "{{ 5 * 5 }}" "25"
]
, testGroup "Division"
[ testCase "24 / 6 = 4" $ do
mkTestHtml [] [] "{{ 24 / 6 }}" "4"
, testCase "3 / 2 = 1.5" $ do
mkTestHtml [] [] "{{ 3 / 2 }}" "1.5"
]
, testGroup "Integer Division"
[ testCase "24 // 6 = 4" $ do
mkTestHtml [] [] "{{ 24 // 6 }}" "4"
, testCase "3 // 2 = 1" $ do
mkTestHtml [] [] "{{ 3 // 2 }}" "1"
]
, testGroup "Modulo"
[ testCase "7 % 3 = 2" $ do
mkTestHtml [] [] "{{ 7 % 3 }}" "1"
]
, testGroup "Iteration"
[ testCase "for x in [ \"foo\", \"bar\", \"baz\" ]: <x>" $ do
mkTestHtml [] [] "{% for x in [ \"foo\", \"bar\", \"baz\" ] %}<{{x}}>{% endfor %}" "<foo><bar><baz>"
, testCase "for x in []: <x> else <no>" $ do
mkTestHtml [] [] "{% for x in [] %}<{{x}}>{% else %}<no>{% endfor %}" "<no>"
, testCase "for x in [a]: <x> else <no>" $ do
mkTestHtml [] [] "{% for x in [\"a\"] %}<{{x}}>{% else %}<no>{% endfor %}" "<a>"
]
, testGroup "The `loop` auto-variable"
[ testCase "loop.cycle" $ do
mkTestHtml [] []
( "{% for x in [\"foo\", \"bar\", \"baz\"] recursive -%}" ++
"({{ loop.cycle(\"red\", \"green\") }}/{{ x }})" ++
"{%- endfor %}"
)
"(red/foo)(green/bar)(red/baz)"
, testCase "recursive loops" $ do
mkTestHtml [] []
( "{% for k, x in {\"a\":{\"b\":null,\"c\":{\"d\":null}}} -%}" ++
"{{ k }}@{{ loop.depth }}({{ loop(x) }})" ++
"{%- endfor %}" :: String
)
"a@1(b@2()c@2(d@3()))"
]
, testGroup "Conditionals"
[ testCase "if true then \"yes\" else \"no\"" $ do
mkTestHtml [] [] "{% if true %}yes{% else %}no{% endif %}" "yes"
, testCase "if false then \"yes\" else if false then \"maybe\" else \"no\"" $ do
mkTestHtml [] [] "{% if false %}yes{% elif false %}maybe{% else %}no{% endif %}" "no"
, testCase "if false then \"yes\" else if true then \"maybe\" else \"no\"" $ do
mkTestHtml [] [] "{% if false %}yes{% elif true %}maybe{% else %}no{% endif %}" "maybe"
]
, testGroup "Comparisons"
[ testCase "if 1 == 1 then \"yes\" else \"no\"" $ do
mkTestHtml [] [] "{% if (1 == 1) %}yes{% else %}no{% endif %}" "yes"
, testCase "if 1 > 0 then \"yes\" else \"no\"" $ do
mkTestHtml [] [] "{% if (1 > 0) %}yes{% else %}no{% endif %}" "yes"
, testCase "if 1 > null then \"yes\" else \"no\"" $ do
mkTestHtml [] [] "{% if (1 > null) %}yes{% else %}no{% endif %}" "no"
, testCase "if 1 < 2 then \"yes\" else \"no\"" $ do
mkTestHtml [] [] "{% if (1 < 2) %}yes{% else %}no{% endif %}" "yes"
, testCase "if null < 1 then \"yes\" else \"no\"" $ do
mkTestHtml [] [] "{% if (null < 1) %}yes{% else %}no{% endif %}" "no"
]
, testGroup "Boolean AND"
[ testCase "AND (both)" $ do
mkTestHtml [] [] "{% if 1 && 2 %}yes{% else %}no{% endif %}" "yes"
, testCase "AND (only one)" $ do
mkTestHtml [] [] "{% if 1 && 0 %}yes{% else %}no{% endif %}" "no"
, testCase "AND (neither)" $ do
mkTestHtml [] [] "{% if 0 && 0 %}yes{% else %}no{% endif %}" "no"
]
, testGroup "Boolean AND"
[ testCase "OR (both)" $ do
mkTestHtml [] [] "{% if 1 || 2 %}yes{% else %}no{% endif %}" "yes"
, testCase "OR (only one)" $ do
mkTestHtml [] [] "{% if 1 || 0 %}yes{% else %}no{% endif %}" "yes"
, testCase "OR (either)" $ do
mkTestHtml [] [] "{% if 0 || 0 %}yes{% else %}no{% endif %}" "no"
]
, testGroup "Slicing brackets"
[ testCase "from/to, both positive" $ do
mkTestHtml [] []
"{{ 'abcdef'[1:2] }}"
"bc"
, testCase "from/to, from negative" $ do
mkTestHtml [] []
"{{ 'abcdef'[-3:2] }}"
"de"
, testCase "from/to, to implicit" $ do
mkTestHtml [] []
"{{ 'abcdef'[1:] }}"
"bcdef"
, testCase "from/to, from implicit" $ do
mkTestHtml [] []
"{{ 'abcdef'[:2] }}"
"ab"
]
, testGroup "Built-in filters/functions"
[ testCase "\"abs\"" $ do
mkTestHtml [] [] "{{ -2|abs }}" "2"
, testGroup "\"any\""
[ testCase "\"any\" (both)" $ do
mkTestHtml [] [] "{% if any(1, 1, true) %}yes{% else %}no{% endif %}" "yes"
, testCase "\"any\" (just one)" $ do
mkTestHtml [] []
"{% if any(0, 1, false) %}yes{% else %}no{% endif %}"
"yes"
, testCase "\"any\" (neither)" $ do
mkTestHtml [] []
"{% if any(0, 0, false) %}yes{% else %}no{% endif %}"
"no"
]
, testGroup "\"all\""
[ testCase "\"all\" (both)" $ do
mkTestHtml [] []
"{% if all(1, 1, true) %}yes{% else %}no{% endif %}"
"yes"
, testCase "\"all\" (just one)" $ do
mkTestHtml [] []
"{% if all(0, 1, false) %}yes{% else %}no{% endif %}"
"no"
, testCase "\"all\" (neither)" $ do
mkTestHtml [] []
"{% if all(0, 0, false) %}yes{% else %}no{% endif %}"
"no"
]
, testGroup "\"ceil\""
[ testCase "14.1" $ do
mkTestHtml [] []
"{{ 14.1|ceil }}"
"15"
, testCase "-14.1" $ do
mkTestHtml [] []
"{{ -14.1|ceil }}"
"-14"
, testCase "-14.8" $ do
mkTestHtml [] []
"{{ -14.8|ceil }}"
"-14"
]
, testCase "\"capitalize\"" $ do
mkTestHtml [] []
"{{ \"this is the end of the world\"|capitalize }}"
"This is the end of the world"
, testGroup "\"center\""
[ testCase "extra space" $ do
mkTestHtml [] []
"{{ \"asdf\"|center(12) }}"
" asdf "
, testCase "no extra space" $ do
mkTestHtml [] []
"{{ \"foobar\"|center(2) }}"
"foobar"
]
, testCase "\"concat\"" $ do
mkTestHtml [] []
"{{ [\"hello\", \"world\"]|concat }}"
"helloworld"
, testGroup "\"contains\""
[ testCase "single match" $ do
mkTestHtml [] []
"{% if ['hello', 'world']|contains('hello') %}yes{% else %}no{% endif %}"
"yes"
, testCase "multi match" $ do
mkTestHtml [] []
"{% if ['hello', 'world']|contains('hello', 'world') %}yes{% else %}no{% endif %}"
"yes"
, testCase "non-match" $ do
mkTestHtml [] []
"{% if ['hello', 'world']|contains('hello', 'you', 'world') %}yes{% else %}no{% endif %}"
"no"
]
, testGroup "\"date\""
[ testCase "format a date" $ do
mkTestHtml [] []
"{{ {'year':2015, 'month':6, 'day':13}|date('%Y-%m-%d') }}"
"2015-06-13"
, testCase "format a list as a date" $ do
mkTestHtml [] []
"{{ [2015, 6, 13, 12, 45, 21]|date('%Y-%m-%d') }}"
"2015-06-13"
, testCase "format a 5-element list as a date" $ do
mkTestHtml [] []
"{{ [2015, 6, 13, 12, 45]|date('%Y-%m-%d') }}"
"2015-06-13"
, testCase "format a 3-element list as a date" $ do
mkTestHtml [] []
"{{ [2015, 6, 13]|date('%Y-%m-%d') }}"
"2015-06-13"
, testCase "use correct default time (noon) with 3-element list" $ do
mkTestHtml [] []
"{{ [2015, 6, 13]|date('%H:%M:%S') }}"
"12:00:00"
, testCase "format a string as a date" $ do
mkTestHtml [] []
"{{ '2015-06-13 12:05:43'|date('%Y-%m-%d') }}"
"2015-06-13"
, testCase "format a string as a date (JSON-style formatting)" $ do
mkTestHtml [] []
"{{ '2015-06-13T12:05:43Z'|date('%Y-%m-%d %H:%M:%S') }}"
"2015-06-13 12:05:43"
, testCase "format a string as a time-of-day" $ do
mkTestHtml [] []
"{{ '2015-06-13 12:05:43'|date('%H-%M-%S') }}"
"12-05-43"
, testCase "format a string as a date, with timezone" $ do
mkTestHtml [] []
"{{ '2015-06-13 12:05:43-05:00'|date('%Y-%m-%d %z') }}"
"2015-06-13 -0500"
, testCase "format a local-time string as a date + time, with explicit timezone" $ do
mkTestHtml [] []
"{{ '2015-06-13 12:05:43'|date('%Y-%m-%d %H:%M %z', tz='+0200') }}"
"2015-06-13 12:05 +0200"
, testCase "format a zoned-time string as a date, with explicit timezone" $ do
mkTestHtml [] []
"{{ '2015-06-13 12:05:43-05:00'|date('%Y-%m-%d %H:%M %z', tz='+0200') }}"
"2015-06-13 19:05 +0200"
, testCase "use a custom locale" $ do
sillyLocale <- loadSillyLocale
mkTestHtml
[("silly", toGVal (sillyLocale :: Maybe JSON.Value))]
[]
"{{ '2015-06-13 12:05:43'|date('%c', locale=silly) }}"
"It be The Day Of Saturn, in the year 15, the clock striketh 12"
]
, testGroup "\"default\""
[ testCase "trigger default" $ do
mkTestHtml [] []
"{{ 0|default(\"hi\") }}"
"hi"
, testCase "use truthy value" $ do
mkTestHtml [] []
"{{ \"hi\"|default(\"nope\") }}"
"hi"
]
, testCase "\"difference\"" $ do
mkTestHtml [] []
"{{ difference(5,2) }}"
"3"
, testGroup "\"dictsort\""
[ testCase "by key" $ do
mkTestHtml [] []
"{{ dictsort({4:4, 1:5}, by='key') }}"
"54"
, testCase "by value" $ do
mkTestHtml [] []
"{{ dictsort({4:4, 1:5}, by='value') }}"
"45"
]
, testGroup "\"escape\""
[ testCase "single item" $ do
mkTestHtml [] []
"{{ escape('<')|raw }}"
"<"
, testCase "multiple items" $ do
mkTestHtml [] []
"{{ escape('<', '>')|raw }}"
"<>"
]
, testGroup "\"equals\""
[ testCase "all equal" $ do
mkTestHtml [] []
"{% if equals(1, 1, 1) %}yes{% else %}no{% endif %}"
"yes"
, testCase "some equal" $ do
mkTestHtml [] []
"{% if equals(1, 1, 2) %}yes{% else %}no{% endif %}"
"no"
]
, testGroup "\"eval\""
[ testCase "simple" $ do
mkTestHtml [] []
"{{ eval('{% set x = 1 %}{{x}}') }}"
"1"
, testCase "with extra state" $ do
mkTestHtml [] []
"{{ eval('{{x}}', { 'x': 1 }) }}"
"1"
, testCase "outside state does not bleed into eval()" $ do
mkTestHtml [] []
"{% set x = 1 %}{{ eval('{{x}}') }}"
""
, testCase "standard functions available inside eval" $ do
mkTestHtml [] []
"{{ eval(\"{{'foobar'|capitalize()}}\") }}"
"Foobar"
]
, testGroup "\"filesizeformat\""
[ testCase "bytes" $ do
mkTestHtml [] []
"{{ 100|filesizeformat }}"
"100 B"
, testCase "kilobytes" $ do
mkTestHtml [] []
"{{ 12000|filesizeformat }}"
"12 kB"
, testCase "megabytes" $ do
mkTestHtml [] []
"{{ 1500000|filesizeformat }}"
"1.5 MB"
, testCase "bytes (2-based)" $ do
mkTestHtml [] []
"{{ 100|filesizeformat(true) }}"
"100 B"
, testCase "kibibytes" $ do
mkTestHtml [] []
"{{ 12000|filesizeformat(true) }}"
"11.7 kiB"
, testCase "mebibytes" $ do
mkTestHtml [] []
"{{ 1500000|filesizeformat(true) }}"
"1.4 MiB"
]
, testGroup "\"filter\""
[ testCase "simple case" $ do
mkTestHtml [] []
"{{ [1, 0, 3]|filter(int) }}"
"13"
, testCase "with extra argument" $ do
mkTestHtml [] []
"{{ [1, 2, 3]|filter(greater, 2) }}"
"3"
]
, testGroup "\"format\""
[ testCase "jinja.pocoo.org example" $ do
mkTestHtml [] []
"{{ \"%s - %s\"|format('Hello?', 'Foo!') }}"
"Hello? - Foo!"
]
, testGroup "\"not-equals\""
[ testCase "all equal" $ do
mkTestHtml [] []
"{% if nequals(1, 1, 1) %}yes{% else %}no{% endif %}"
"no"
, testCase "not all equal" $ do
mkTestHtml [] []
"{% if nequals(1, 1, 2) %}yes{% else %}no{% endif %}"
"yes"
]
, testGroup "\"floor\""
[ testCase "14.1" $ do
mkTestHtml [] []
"{{ 14.1|floor }}"
"14"
, testCase "14.8" $ do
mkTestHtml [] []
"{{ 14.8|floor }}"
"14"
, testCase "-14.1" $ do
mkTestHtml [] []
"{{ -14.1|floor }}"
"-15"
, testCase "-14.8" $ do
mkTestHtml [] []
"{{ -14.8|floor }}"
"-15"
]
, testGroup "\"int\""
[ testCase "14.1" $ do
mkTestHtml [] []
"{{ 14.1|int }}"
"14"
, testCase "14.8" $ do
mkTestHtml [] []
"{{ 14.8|int }}"
"14"
, testCase "-14.1" $ do
mkTestHtml [] []
"{{ -14.1|int }}"
"-14"
, testCase "-14.8" $ do
mkTestHtml [] []
"{{ -14.8|int }}"
"-14"
]
-- \"int_ratio\"
-- TODO
-- \"iterable\"
-- TODO
, testCase "\"length\"" $ do
mkTestHtml [] []
"{{ [1,2,3]|length }}"
"3"
-- \"modulo\"
-- \"num\"
-- TODO
-- \"printf\"
, testGroup "\"printf\""
[ testCase "%s, passed as int" $ do
mkTestHtml [] []
"{{ printf(\"%i\", 1) }}"
"1"
, testCase "%s, passed as string" $ do
mkTestHtml [] []
"{{ printf(\"%i\", \"1\") }}"
"1"
, testCase "%i, passed as float" $ do
mkTestHtml [] []
"{{ printf(\"%i\", 1.3) }}"
"1"
, testCase "%f, passed as int" $ do
mkTestHtml [] []
"{{ printf(\"%f\", 1) }}"
"1.0"
, testCase "%.3f, passed as int" $ do
mkTestHtml [] []
"{{ printf(\"%.3f\", 1) }}"
"1.000"
, testCase "%s, string" $ do
mkTestHtml [] []
"{{ printf(\"%s\", \"Hello\") }}"
"Hello"
]
, testCase "\"product\"" $ do
mkTestHtml [] []
"{{ product(1,2,3) }}"
"6"
, testCase "\"ratio\"" $ do
mkTestHtml [] []
"{{ ratio(6, 1.5, 2) }}"
"2"
, testGroup "\"round\""
[ testCase "14.1" $ do
mkTestHtml [] []
"{{ 14.1|round }}"
"14"
, testCase "14.8" $ do
mkTestHtml [] []
"{{ 14.8|round }}"
"15"
, testCase "-14.1" $ do
mkTestHtml [] []
"{{ -14.1|round }}"
"-14"
, testCase "-14.8" $ do
mkTestHtml [] []
"{{ -14.8|round }}"
"-15"
]
-- \"show\"
-- TODO
, testCase "\"str\"" $ do
mkTestHtml [] []
"{{ str(123) }}"
"123"
, testCase "\"sum\"" $ do
mkTestHtml [] []
"{{sum(1, 2, 3)}}"
"6"
, testGroup "\"truncate\""
[ testCase "14.1" $ do
mkTestHtml [] []
"{{ 14.1|truncate }}"
"14"
, testCase "14.8" $ do
mkTestHtml [] []
"{{ 14.8|truncate }}"
"14"
, testCase "-14.1" $ do
mkTestHtml [] []
"{{ -14.1|truncate }}"
"-14"
, testCase "-14.8" $ do
mkTestHtml [] []
"{{ -14.8|truncate }}"
"-14"
]
, testCase "\"urlencode\"" $ do
mkTestHtml [] []
"{{ \"a/b c\"|urlencode }}"
"a%2Fb%20c"
, testGroup "\"sort\""
[ testCase "simple" $ do
mkTestHtml [] []
"{{ [2,3,1]|sort }}"
"123"
, testCase "reverse" $ do
mkTestHtml [] []
"{{ [2,3,1]|sort(reverse=true) }}"
"321"
, testCase "sort by key" $ do
mkTestHtml [] []
"{{ ([ {\"age\":30, \"name\":\"zzz\"}, {\"age\":41, \"name\":\"aaa\"} ]|sort(by=\"name\"))[0]['name'] }}"
"aaa"
, testCase "sort by key, reverse" $ do
mkTestHtml [] []
"{{ ([ {\"age\":30, \"name\":\"zzz\"}, {\"age\":41, \"name\":\"aaa\"} ]|sort(by=\"age\", reverse=true))[0]['name'] }}"
"aaa"
, testCase "sort dictionary by keys" $ do
mkTestHtml [] []
"{{ {'foo': 1, 'bar': 2}|sort(by='__key') }}"
"21"
, testCase "sort by a projection function" $ do
mkTestHtml [] []
"{{ ['foo', 'bar', 'quux']|sort(by=(str) -> str[1:]) }}"
"barfooquux"
, testCase "sort by a path" $ do
mkTestHtml [] []
"{{ [{'main':{'sub': 2}, 'msg':'no'},{'main':{'sub': 1}, 'msg':'yes'}]|sort(by=['main', 'sub']) }}"
"1yes2no"
]
, testGroup "\"slice\""
[ testCase "full positional args" $ do
mkTestHtml [] []
"{{ [1, 2, 3, 4, 5]|slice(1,3) }}"
"234"
, testCase "implicit 'to end'" $ do
mkTestHtml [] []
"{{ [1, 2, 3, 4, 5]|slice(1) }}"
"2345"
, testCase "full named args" $ do
mkTestHtml [] []
"{{ [1, 2, 3, 4, 5]|slice(length=3,start=1) }}"
"234"
, testCase "negative offset" $ do
mkTestHtml [] []
"{{ [1, 2, 3, 4, 5]|slice(start=-1) }}"
"5"
, testCase "call on string subject" $ do
mkTestHtml [] []
"{{ \"12345\"|slice(1,3) }}"
"234"
]
, testGroup "\"replace\""
[ testCase "simple case" $ do
mkTestHtml [] []
"{{ replace('foobar', 'a', 'e') }}"
"foober"
, testCase "multiple replacements" $ do
mkTestHtml [] []
"{{ replace('foobar', 'o', 'e') }}"
"feebar"
, testCase "longer replacements" $ do
mkTestHtml [] []
"{{ replace('foobar', 'oo', 'e') }}"
"febar"
, testCase "deletion" $ do
mkTestHtml [] []
"{{ replace('foobar', 'o') }}"
"fbar"
]
]
, testGroup "Setting variables"
[ testCase "plain" $ do
mkTestHtml [] []
"{% set x = \"world\" %}Hello, {{ x }}!"
"Hello, world!"
, testCase "self-referential" $ do
mkTestHtml [] []
"{% set x = { \"foo\":\"bar\" } %}{% set x = x[\"foo\"] %}Hello, {{ x }}!"
"Hello, bar!"
]
, testGroup "HTML encoding"
[ testCase "no encoding outside of constructs" $ do
mkTestHtml [] []
"<foo bar=\"baz\"/>Ampersand is '&'</foo>."
"<foo bar=\"baz\"/>Ampersand is '&'</foo>."
, testCase "auto-encode inside interpolations" $ do
mkTestHtml [] []
"{{ \"<foo bar=\\\"baz\\\"/>amp is '&'</foo>.\" }}"
"<foo bar="baz"/>amp is '&'</foo>."
, testCase "raw filter bypasses encoding" $ do
mkTestHtml [] []
"{{ \"<a>&\"|raw }}"
"<a>&"
]
, testGroup "Includes"
[ testCase "include plain" $ do
mkTestHtml [] [("./features-included.html", "This is an included template")]
"{% include 'features-included.html' %}"
"This is an included template"
, testCase "include with a variable" $ do
mkTestHtml [] [("./features-included.html", "Hello, {{ user }}!")]
"{% set user='world' %}{% include 'features-included.html' %}"
"Hello, world!"
, testCase "include referencing an included variable" $ do
mkTestHtml [] [("./features-included.html", "{% set user = 'foobar' %}")]
"{% include 'features-included.html' %}Hello, {{ user }}!"
"Hello, foobar!"
]
, testGroup "Explicit Local Scopes"
[ testCase "baseline" $ do
mkTestHtml [] [] "{% set bedazzle = \"no\" %}{{ bedazzle }}" "no"
, testCase "inside local scope" $ do
mkTestHtml [] [] "{% set bedazzle = \"no\" %}{% scope %}{% set bedazzle = \"ya\" %}{{ bedazzle }}{% endscope %}" "ya"
, testCase "after exiting local scope" $ do
mkTestHtml [] [] "{% set bedazzle = \"no\" %}{% scope %}{% set bedazzle = \"ya\" %}{% endscope %}{{ bedazzle }}" "no"
]
, testGroup "Macros"
[ testCase "simple" $ do
mkTestHtml [] []
"{% macro foobar -%}baz{%- endmacro %}{{ foobar() }}"
"baz"
, testCase "with args" $ do
mkTestHtml [] []
"{% macro foobar2(baz) -%}{{ baz }}oo{%- endmacro %}{{ foobar2(boink=\"nope\", baz=\"blabber\") }}"
"blabberoo"
]
, testGroup "Lambdas"
[ testCase "single arg" $ do
mkTestHtml [] [] "{{ ((x) -> x * x)(2) }}" "4"
, testCase "two args" $ do
mkTestHtml [] [] "{{ ((greeting, name) -> greeting ~ \", \" ~ name ~ \"!\")(\"Hello\", \"world\") }}" "Hello, world!"
]
, testGroup "Ternary operator"
[ testCase "C syntax" $ do
mkTestHtml [] [] "{{ 1 ? \"yes\" : \"no\" }}" "yes"
, testCase "python syntax" $ do
mkTestHtml [] [] "{{ \"yes\" if 1 else \"no\" }}" "yes"
, testCase "C syntax, nested, true/false" $ do
mkTestHtml [] [] "{{ true ? false ? \"a\" : \"b\" : \"c\" }}" "b"
, testCase "Python syntax, nested, true/false" $ do
mkTestHtml [] [] "{{ \"a\" if false else \"b\" if true else \"c\" }}" "b"
, testCase "C syntax, nested, true/true" $ do
mkTestHtml [] [] "{{ true ? true ? \"a\" : \"b\" : \"c\" }}" "a"
, testCase "Python syntax, nested, true/true" $ do
mkTestHtml [] [] "{{ false ? true ? \"a\" : \"b\" : \"c\" }}" "c"
]
, testGroup "Call syntax"
[ testCase "\"caller\"" $ do
mkTestHtml [] [] "{% macro foobar3(a) -%}{{ a }}({{ caller(\"asdf\") }}){%- endmacro %}{% call (a) foobar3(\"hey\") %}<{{a}}>{% endcall %}" "hey(<asdf>)"
]
, testGroup "Inheritance"
[ testCase "inheritance" $ do
mkTestHtml
[]
[ ("./inherit-child.html", "{% extends \"inherit-parent.html\" %}{% block test -%}This is right.{% endblock %}")
, ("./inherit-parent.html", "{%- block test -%}This is wrong.{% endblock -%}")
]
"{% include \"inherit-child.html\" %}"
"This is right."
, testCase "multi-tier inheritance" $ do
mkTestHtml
[]
[ ( "./inherit-parent.html"
, "{%- extends \"inherit-grandparent.html\" %}" ++
"{%- block outer -%}Outer{%- block inner -%}{%- endblock -%}{%- endblock -%}"
)
, ( "./inherit-grandparent.html"
, "*{% block outer -%}{%- endblock %}*"
)
]
( "{%- extends \"inherit-parent.html\" -%}" ++
"{%- block inner %} Space{%- endblock -%}"
)
"*Outer Space*"
]
, testGroup "Non-HTML Output"
[ testCase "text" $ do
mkTestText
[]
[]
"<>{{ '<>' }}<>"
"<><><>"
, testCase "json" $ do
mkTestJSON
[]
[]
"{{ '<>' }}{{ 1 }}{{ {'foo': true} }}"
"[\"<>\",1,{\"foo\":true}]"
]
]
mkTestHtml :: [(VarName, GVal (Run IO Html))]
-> [(FilePath, String)]
-> String
-> Text
-> Assertion
mkTestHtml = mkTest makeContextHtmlM htmlSource
mkTestText :: [(VarName, GVal (Run IO Text))]
-> [(FilePath, String)]
-> String
-> Text
-> Assertion
mkTestText = mkTest makeContextTextM id
mkTestJSON :: [(VarName, GVal (Run IO [JSON.Value]))]
-> [(FilePath, String)]
-> String
-> Text
-> Assertion
mkTestJSON = mkTest
(\l w -> makeContextM' l w toJSONSingleton) encodeText
where
toJSONSingleton = (:[]) . JSON.toJSON
encodeText :: JSON.ToJSON a => a -> Text
encodeText = Text.pack . LUTF8.toString . JSON.encode
mkTest :: (Monoid a, ToGVal (Run IO a) a)
=> ((VarName -> Run IO a (GVal (Run IO a))) -> (a -> IO ()) -> GingerContext IO a) -- ^ mkContextM flavor
-> (a -> Text) -- ^ Convert a to Text for output
-> [(VarName, GVal (Run IO a))] -- ^ Context dictionary
-> [(FilePath, String)] -- ^ Lookup table for includes
-> String -- ^ Template source
-> Text -- ^ Expected output
-> Assertion
mkTest mContext valToText contextDict includeLookup src expected = do
let resolver srcName = return $ lookup srcName includeLookup
template <- either (fail . show) return =<< parseGinger resolver Nothing src
output <- newIORef mempty
let write h = modifyIORef output (<> h)
let context = mContext
(\key -> return $ fromMaybe def (lookup key contextDict))
write
runGingerT context (optimize template)
actual <- valToText <$> readIORef output
assertEqual "" expected actual
loadSillyLocale :: IO (Maybe JSON.Value)
loadSillyLocale = do
JSON.decode <$> LBS.readFile "test/fixtures/silly-locale.json"