packages feed

granite-0.5.0.0: test/PipelineSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module PipelineSpec (spec) where

import Data.Text qualified as Text

import Granite.Render.Pipeline
import Granite.Render.Scene (Scene (..))
import Granite.Spec
import Test.Hspec

-- A scatter chart equivalent to the README example: two named series
-- of (x, y) data, rendered through the new IR pipeline.
scatterChart :: Chart
scatterChart =
    emptyChart
        { chartTitle = Just "Random points"
        , chartSize = SizeChars 68 22
        , chartLayers =
            [ pointLayer "A" [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16)]
            , pointLayer "B" [(0, 1), (1, 3), (2, 6), (3, 10), (4, 15)]
            ]
        }
  where
    pointLayer name pts =
        let xs = map fst pts
            ys = map snd pts
            df =
                fromColumns
                    [ ("x", ColNum xs)
                    , ("y", ColNum ys)
                    ]
         in (defLayer GeomPoint)
                { layerData = Just df
                , layerMapping =
                    emptyMapping
                        { aesX = Just (ColumnRef "x")
                        , aesY = Just (ColumnRef "y")
                        , aesGroup = Just (ColumnRef name)
                        }
                }

spec :: Spec
spec = describe "Granite.Render.Pipeline" $ do
    describe "scatter Chart through the IR" $ do
        it "produces a non-empty Scene with both axes" $ do
            let scene = chartToScene scatterChart
            sceneWidth scene `shouldSatisfy` (> 0)
            sceneHeight scene `shouldSatisfy` (> 0)
            length (sceneMarks scene) `shouldSatisfy` (> 0)

        it "terminal output contains the title" $ do
            let term = renderChartTerminal scatterChart
            term `shouldSatisfy` Text.isInfixOf "Random points"

        it "SVG output contains the title and viewBox" $ do
            let svg = renderChartSvg scatterChart
            svg `shouldSatisfy` Text.isInfixOf "Random points"
            svg `shouldSatisfy` Text.isInfixOf "viewBox"

        it "SVG output has axis structure (lines + tick text)" $ do
            let svg = renderChartSvg scatterChart
            -- Axes are emitted as <line> (since the chrome
            -- gridline/tick refactor); <polyline> still appears for
            -- data layers.
            svg `shouldSatisfy` Text.isInfixOf "<line"
            svg `shouldSatisfy` Text.isInfixOf "<text"

        it "SVG output has at least one circle per series" $ do
            let svg = renderChartSvg scatterChart
                ncircles = Text.count "<circle" svg
            ncircles `shouldSatisfy` (>= 5 * 2) -- 5 points × 2 series
        it "SVG output has a legend swatch per series" $ do
            let svg = renderChartSvg scatterChart
                nrects = Text.count "<rect" svg
            -- 1 background + 2 legend swatches; allow more from future chrome
            nrects `shouldSatisfy` (>= 3)

        it "terminal line count is similar to legacy output" $ do
            let term = renderChartTerminal scatterChart
                lineCount = length (Text.lines term)
            -- For a 68×22 char canvas plus title + bottom labels we expect
            -- roughly 24 lines (22 plot rows + title + axis labels).
            lineCount `shouldSatisfy` (>= 15)
            lineCount `shouldSatisfy` (<= 40)

    describe "Log-scale chart" $ do
        let logChart =
                scatterChart
                    { chartScales =
                        defScales{scaleY = SLog Base10 defScaleOpts}
                    , chartTitle = Just "Log Y"
                    }
        it "renders without error and includes log-friendly breaks" $ do
            let svg = renderChartSvg logChart
            svg `shouldSatisfy` Text.isInfixOf "Log Y"
            svg `shouldSatisfy` Text.isInfixOf "<svg"

    describe "Empty chart" $ do
        it "renders an empty chart without crashing" $ do
            let svg = renderChartSvg emptyChart
                term = renderChartTerminal emptyChart
            svg `shouldSatisfy` Text.isInfixOf "<svg"
            Text.length term `shouldSatisfy` (>= 0)

    describe "SizeResponsive" $ do
        let responsive =
                scatterChart{chartSize = SizeResponsive 2.0}
        let fixed =
                scatterChart{chartSize = SizeChars 60 20}

        it "drops absolute width/height for SizeResponsive" $ do
            let svg = renderChartSvg responsive
            svg `shouldSatisfy` Text.isInfixOf "viewBox=\"0 0"
            svg `shouldSatisfy` Text.isInfixOf "preserveAspectRatio"
            svg `shouldSatisfy` Text.isInfixOf "width=\"100%\""
            svg `shouldSatisfy` Text.isInfixOf "style=\"height:auto"

        it "keeps absolute width/height for SizeChars / SizePixels" $ do
            let svg = renderChartSvg fixed
                -- inspect just the opening <svg ...> tag, not the body
                openSvg = Text.takeWhile (/= '>') (Text.dropWhile (/= '<') svg)
            openSvg `shouldSatisfy` Text.isInfixOf "viewBox"
            openSvg `shouldSatisfy` (not . Text.isInfixOf "preserveAspectRatio")
            openSvg `shouldSatisfy` (not . Text.isInfixOf "width=\"100%\"")