keiki-0.1.0.0: test/Keiki/Render/ValidateSpec.hs
-- | Unit tests for "Keiki.Render.Validate" (EP-66). Pure, hspec-only:
-- each crafted diagram produces exactly the listed warning value, and a
-- well-formed diagram produces the empty list.
--
-- See @docs/plans/66-pure-mermaid-diagram-and-atlas-validation-helpers.md@.
module Keiki.Render.ValidateSpec (spec) where
import Data.Text (Text)
import Data.Text qualified as T
import Keiki.Render.Validate
( MermaidValidationOptions (..),
MermaidValidationWarning (..),
defaultMermaidValidationOptions,
validateMermaidAtlas,
validateMermaidDiagram,
)
import Test.Hspec
spec :: Spec
spec = do
describe "validateMermaidDiagram" $ do
it "passes a well-formed diagram" $
validateMermaidDiagram defaultMermaidValidationOptions goodDiagram
`shouldBe` []
it "flags a missing stateDiagram-v2 header" $
validateMermaidDiagram defaultMermaidValidationOptions noHeader
`shouldBe` [MissingStateDiagramHeader]
it "flags an empty diagram (header with nothing under it)" $
validateMermaidDiagram defaultMermaidValidationOptions onlyHeader
`shouldBe` [EmptyDiagram]
it "flags an over-length label with line, length and text" $
validateMermaidDiagram tinyBudget goodDiagram
`shouldBe` [LabelTooLong {warnLine = 3, warnLength = 13, warnLabel = T.pack "A / LongEvent"}]
it "flags a suspicious unescaped character in a label" $
validateMermaidDiagram defaultMermaidValidationOptions pipeLabel
`shouldBe` [SuspiciousUnescapedChar {warnLine = 3, warnChar = '|', warnLabel = T.pack "Cmd / a|b"}]
it "does not flag the deliberate <br/> tag keiki emits" $
validateMermaidDiagram defaultMermaidValidationOptions brDiagram
`shouldBe` []
it "flags a duplicate declared state id but not endpoint recurrence" $
validateMermaidDiagram defaultMermaidValidationOptions dupDecls
`shouldBe` [DuplicateStateId {warnStateId = T.pack "S1"}]
describe "validateMermaidAtlas" $
it "aggregates warnings across fenced mermaid blocks in block order" $
validateMermaidAtlas defaultMermaidValidationOptions atlas
`shouldBe` [MissingStateDiagramHeader]
-- | A well-formed two-state diagram; its line-3 label is 13 characters.
goodDiagram :: Text
goodDiagram =
T.intercalate
(T.pack "\n")
[ "stateDiagram-v2",
" [*] --> S1",
" S1 --> S2 : A / LongEvent",
" S2 --> [*]"
]
-- | 'defaultMermaidValidationOptions' with a 5-character label budget.
tinyBudget :: MermaidValidationOptions
tinyBudget = defaultMermaidValidationOptions {maxLabelLength = Just 5}
-- | No @stateDiagram-v2@ header line.
noHeader :: Text
noHeader =
T.intercalate
(T.pack "\n")
[ " [*] --> S1",
" S1 --> [*]"
]
-- | A header with no body.
onlyHeader :: Text
onlyHeader = T.pack "stateDiagram-v2"
-- | A label carrying a single denylisted character (the pipe @|@).
pipeLabel :: Text
pipeLabel =
T.intercalate
(T.pack "\n")
[ "stateDiagram-v2",
" [*] --> S1",
" S1 --> S2 : Cmd / a|b",
" S2 --> [*]"
]
-- | A label whose only angle brackets come from a deliberate @<br/>@ tag,
-- which the validator exempts.
brDiagram :: Text
brDiagram =
T.intercalate
(T.pack "\n")
[ "stateDiagram-v2",
" [*] --> S1",
" S1 --> S2 : Cmd / A<br/>B",
" S2 --> [*]"
]
-- | The id @S1@ is declared twice (with conflicting display labels) and
-- also recurs as a transition endpoint; only the declaration clash is a
-- duplicate.
dupDecls :: Text
dupDecls =
T.intercalate
(T.pack "\n")
[ "stateDiagram-v2",
" state \"First\" as S1",
" state \"Second\" as S1",
" [*] --> S1",
" S1 --> [*]"
]
-- | A two-section atlas: one clean block, one block missing its header.
atlas :: Text
atlas =
T.intercalate
(T.pack "\n")
[ "## Good",
"",
"```mermaid",
"stateDiagram-v2",
" [*] --> S1",
" S1 --> [*]",
"```",
"",
"## Bad",
"",
"```mermaid",
" [*] --> S1",
" S1 --> [*]",
"```"
]