packages feed

reflex-vty-1.3.0.0: test/Reflex/Vty/StyleSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Reflex.Vty.StyleSpec (spec) where

import Data.Maybe (isJust)
import qualified Data.Text as T
import qualified Graphics.Vty as V
import qualified Graphics.Vty.Attributes.Color as V.Color
import qualified Graphics.Vty.Image as V.Image
import Test.Hspec

import Reflex.Vty.Style
import Reflex.Vty.Test.Snapshot (Grid (..), RenderSpan (..), imageText, imageToGrid)

spec :: Spec
spec = describe "Reflex.Vty.Style" $ do
  describe "inherit" $ do
    let parent = withForeground red . withBold . withWidth 10 $ def
    it "keeps the child's foreground when set" $ do
      let child = withForeground blue def
      _style_foreground (inherit parent child) `shouldBe` Just blue
    it "inherits the parent's foreground when the child's is Nothing" $ do
      let child = def
      _style_foreground (inherit parent child) `shouldBe` Just red
    it "keeps the child's width when set" $ do
      let child = withWidth 5 def
      _style_width (inherit parent child) `shouldBe` Just 5
    it "inherits the parent's width when the child's is Nothing" $ do
      let child = def
      _style_width (inherit parent child) `shouldBe` Just 10
    it "inherits the parent's bold flag when the child's is Nothing" $ do
      let child = def
      _style_bold (inherit parent child) `shouldBe` Just True

  describe "applyAttr" $ do
    it "applies the foreground color" $ do
      let s = withForeground red def
      V.attrForeColor (applyAttr s V.defAttr) `shouldBe` V.SetTo red
    it "applies the background color" $ do
      let s = withBackground blue def
      V.attrBackColor (applyAttr s V.defAttr) `shouldBe` V.SetTo blue
    it "applies bold when Just True" $ do
      let s = withBold def
      V.hasStyle (V.styleMask (applyAttr s V.defAttr)) V.bold `shouldBe` True
    it "does not apply bold when Nothing" $ do
      V.hasStyle (V.styleMask (applyAttr def V.defAttr)) V.bold `shouldBe` False
    it "applies underline for UnderlineSingle" $ do
      let s = withUnderline UnderlineSingle def
      V.hasStyle (V.styleMask (applyAttr s V.defAttr)) V.underline `shouldBe` True
    it "does not apply underline for UnderlineNone" $ do
      let s = withUnderline UnderlineNone def
      V.hasStyle (V.styleMask (applyAttr s V.defAttr)) V.underline `shouldBe` False
    it "applies italic" $ do
      let s = withItalic def
      V.hasStyle (V.styleMask (applyAttr s V.defAttr)) V.italic `shouldBe` True
    it "applies faint (dim)" $ do
      let s = withFaint def
      V.hasStyle (V.styleMask (applyAttr s V.defAttr)) V.dim `shouldBe` True
    it "applies blink" $ do
      let s = withBlink def
      V.hasStyle (V.styleMask (applyAttr s V.defAttr)) V.blink `shouldBe` True
    it "applies strikethrough" $ do
      let s = withStrikethrough def
      V.hasStyle (V.styleMask (applyAttr s V.defAttr)) V.strikethrough `shouldBe` True
    it "applies reverse video" $ do
      let s = withReverse def
      V.hasStyle (V.styleMask (applyAttr s V.defAttr)) V.reverseVideo `shouldBe` True
    it "applies the hyperlink URL" $ do
      let s = withHyperlink "https://example.com" def
      V.attrURL (applyAttr s V.defAttr) `shouldBe` V.SetTo "https://example.com"
    it "leaves the base attr unchanged for def" $ do
      applyAttr def V.defAttr `shouldBe` V.defAttr

  describe "mergeAttr" $ do
    let base = V.defAttr `V.withForeColor` red `V.withBackColor` blue
    it "uses overlay's SetTo fields over the base" $ do
      let overlay = V.defAttr `V.withForeColor` green
          result = mergeAttr base overlay
      V.attrForeColor result `shouldBe` V.SetTo green
      V.attrBackColor result `shouldBe` V.SetTo blue
    it "falls through to base when overlay is Default" $ do
      let overlay = V.defAttr
          result = mergeAttr base overlay
      V.attrForeColor result `shouldBe` V.SetTo red
      V.attrBackColor result `shouldBe` V.SetTo blue
    it "falls through to base when overlay is KeepCurrent" $ do
      let overlay = V.Attr {V.attrStyle = V.KeepCurrent, V.attrForeColor = V.KeepCurrent, V.attrBackColor = V.KeepCurrent, V.attrURL = V.KeepCurrent}
          result = mergeAttr base overlay
      V.attrForeColor result `shouldBe` V.SetTo red

  describe "render" $ do
    it "produces a non-empty image for plain text" $ do
      let img = render def "hello"
      V.Image.imageWidth img `shouldSatisfy` (> 0)
      V.Image.imageHeight img `shouldBe` 1
    it "respects withWidth by padding" $ do
      let img = render (withWidth 10 def) "hi"
      V.Image.imageWidth img `shouldBe` 10
    it "respects withHeight by padding" $ do
      let img = render (withHeight 3 def) "hi"
      V.Image.imageHeight img `shouldBe` 3
    it "respects withMaxWidth by clipping" $ do
      let img = render (withMaxWidth 3 def) "hello"
      V.Image.imageWidth img `shouldBe` 3
    it "respects withMaxHeight by clipping" $ do
      let img = render (withMaxHeight 1 def) "hello"
      V.Image.imageHeight img `shouldBe` 1
    it "adds padding around content" $ do
      let s = withPadding 1 1 1 1 def
          img = render s "hi"
      V.Image.imageWidth img `shouldBe` 4
      V.Image.imageHeight img `shouldBe` 3
    it "adds a border around content" $ do
      let s = withBorder singleBorder def
          img = render s "hi"
      V.Image.imageWidth img `shouldBe` 4
      V.Image.imageHeight img `shouldBe` 3
    it "adds margin outside content" $ do
      let s = withMargin 1 1 1 1 def
          img = render s "hi"
      V.Image.imageWidth img `shouldBe` 4
      V.Image.imageHeight img `shouldBe` 3
    it "renders multi-line text with correct height" $ do
      let img = render def "hello\nworld"
      V.Image.imageHeight img `shouldBe` 2
    it "renders multi-line text width as longest line" $ do
      let img = render def "hello\nhi"
      V.Image.imageWidth img `shouldBe` 5
    it "renders wide characters at correct width" $ do
      let img = render def "文"
      V.Image.imageWidth img `shouldBe` 2

  describe "measure" $ do
    it "matches render's width for plain text" $ do
      let s = def
      fst (measure s "hello") `shouldBe` V.Image.imageWidth (render s "hello")
    it "accounts for padding" $ do
      let s = withPadding 2 3 2 3 def
      fst (measure s "hi") `shouldBe` 2 + 3 + 3
    it "accounts for border" $ do
      let s = withBorder singleBorder def
      fst (measure s "hi") `shouldBe` 2 + 2
    it "accounts for margin" $ do
      let s = withMargin 1 1 1 1 def
      fst (measure s "hi") `shouldBe` 2 + 2
    it "measures multi-line text with correct height" $ do
      snd (measure def "hello\nworld") `shouldBe` 2
    it "measures multi-line text with correct width" $ do
      fst (measure def "hello\nhi") `shouldBe` 5
    it "measures multi-line text with matching render dimensions" $ do
      let s = withBorder singleBorder def
          (w, h) = measure s "hello\nworld"
          img = render s "hello\nworld"
      w `shouldBe` V.Image.imageWidth img
      h `shouldBe` V.Image.imageHeight img
    it "measures wide characters at display width" $ do
      fst (measure def "文") `shouldBe` 2
    it "measures mixed wide and narrow characters" $ do
      fst (measure def "a文b") `shouldBe` 4
    it "measures empty content as zero width, one line" $ do
      measure def "" `shouldBe` (0, 1)

  describe "withTransform" $ do
    it "applies transform before rendering" $ do
      let img = render (withTransform T.toUpper def) "hello"
      V.Image.imageWidth img `shouldBe` 5

    it "renders transformed text" $ do
      let img = render (withTransform (T.replace "a" "AA") def) "cat"
      V.Image.imageWidth img `shouldBe` 4

  describe "withTabWidth" $ do
    it "expands tabs to spaces" $ do
      let img = render (withTabWidth 4 def) "a\tb"
      V.Image.imageWidth img `shouldBe` 6

    it "does not expand tabs when not set" $ do
      let img = render def "a\tb"
      V.Image.imageWidth img `shouldSatisfy` (< 6)

  describe "withMarginBackground" $ do
    it "produces correct dimensions with margin background" $ do
      let s = withMarginBackground red . withMargin 1 1 1 1 $ def
          (w, h) = measure s "hi"
      w `shouldBe` 4
      h `shouldBe` 3

  describe "new border presets" $ do
    it "innerHalfBorder has all sides" $ do
      _border_top innerHalfBorder `shouldBe` Just '▀'
      _border_left innerHalfBorder `shouldBe` Just '▌'
    it "outerHalfBlockBorder has all sides" $ do
      _border_top outerHalfBlockBorder `shouldBe` Just '▔'
      _border_left outerHalfBlockBorder `shouldBe` Just '▏'
    it "renders innerHalfBorder" $ do
      let img = render (withBorder innerHalfBorder def) "X"
      V.Image.imageWidth img `shouldBe` 3
      V.Image.imageHeight img `shouldBe` 3

  describe "image composition coordinates" $ do
    let imgA = render def "ab"
        imgTall = render def "x\ny"
    it "placeHorizontal aligns content within the padded width" $ do
      imageText (placeHorizontal HAlignLeft 5 imgA) `shouldBe` ["ab   "]
      imageText (placeHorizontal HAlignCenter 5 imgA) `shouldBe` [" ab  "]
      imageText (placeHorizontal HAlignRight 5 imgA) `shouldBe` ["   ab"]
    it "placeVertical aligns content within the padded height" $ do
      imageText (placeVertical VAlignTop 3 imgA) `shouldBe` ["ab", "  ", "  "]
      imageText (placeVertical VAlignBottom 3 imgA) `shouldBe` ["  ", "  ", "ab"]
    it "place combines both axes" $ do
      imageText (place HAlignRight VAlignBottom 4 2 imgA) `shouldBe` ["    ", "  ab"]
    it "joinHorizontal pads to the tallest and concatenates widths" $ do
      let joined = joinHorizontal VAlignTop [imgA, imgTall]
      V.imageWidth joined `shouldBe` 3
      V.imageHeight joined `shouldBe` 2
      imageText joined `shouldBe` ["abx", "  y"]
    it "joinVertical pads to the widest and stacks heights" $ do
      let joined = joinVertical HAlignRight [imgA, imgTall]
      V.imageWidth joined `shouldBe` 2
      V.imageHeight joined `shouldBe` 3
      imageText joined `shouldBe` ["ab", " x", " y"]
    it "placeHorizontal leaves oversized images untouched" $ do
      imageText (placeHorizontal HAlignCenter 1 imgA) `shouldBe` ["ab"]

  describe "hyperlinks" $ do
    it "renders the URL attribute onto the text" $ do
      let img = render (withHyperlink "https://example.com" def) "link"
          grid = imageToGrid img
      case gridRows grid of
        [[s]] -> do
          V.fixedURL (renderSpanAttr s) `shouldBe` Just "https://example.com"
          T.unpack (renderSpanText s) `shouldBe` "link"
        _ -> expectationFailure "expected one row with one span"