packages feed

reflex-vty-1.3.0.0: test/Reflex/Vty/Widget/BoxSpec.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Reflex.Vty.Widget.BoxSpec (spec) where

import qualified Data.Text as T
import qualified Graphics.Vty as V
import Reflex
import Test.Hspec

import Data.Text.Zipper (TextAlignment (..))
import Reflex.Vty.Host
import Reflex.Vty.Test.AppHarness
import Reflex.Vty.Test.Snapshot (Grid (..), RenderSpan (..))
import Reflex.Vty.Widget (blank, tellImages)
import Reflex.Vty.Widget.Box
import Reflex.Vty.Widget.Input.Mouse
import Reflex.Vty.Widget.Text (text)

-- | The full text of one rendered row of a frame.
frameRow :: Frame o -> Int -> String
frameRow f y = case drop y (gridRows (frameGrid f)) of
  (r : _) -> T.unpack (T.concat (map renderSpanText r))
  [] -> ""

-- | The newest frame drawn so far (for a static app, the initial frame).
lastFrame :: AppDriver c o -> IO (Frame o)
lastFrame dr = last <$> driverFrames dr

----------------------------------------------------------------------------
-- Applications under test
----------------------------------------------------------------------------

-- | A full-screen single-style box containing the text \"hi\".
boxApp :: TestApp () ()
boxApp _cmd = do
  _ <- boxStatic singleBoxStyle (text (pure "hi"))
  pure (pure (), never)

-- | A full-screen titled box whose title alignment is driven by commands.
titleApp :: TestApp TextAlignment ()
titleApp cmd = do
  align <- holdDyn TextAlignment_Center cmd
  _ <- boxTitle (current align) (pure singleBoxStyle) (pure "Hi") blank
  pure (pure (), never)

-- | A full-screen titled box whose title is wider than the box.
clipTitleApp :: TestApp () ()
clipTitleApp _cmd = do
  _ <- boxTitle (pure TextAlignment_Left) (pure singleBoxStyle) (pure "ABCDEFGHIJKLM") blank
  pure (pure (), never)

-- | A full-screen box whose child records the region-relative coordinates
-- of left-button presses.
translateApp :: TestApp () (Int, Int)
translateApp _cmd = do
  pos <-
    box (pure singleBoxStyle) $ do
      md <- mouseDown V.BLeft
      holdDyn (-1, -1) (_mouseDown_coordinates <$> md)
  pure (pos, never)

-- | A full-screen box whose child tries to paint far beyond the box.
clipApp :: TestApp () ()
clipApp _cmd = do
  _ <-
    boxStatic singleBoxStyle $
      tellImages (pure [V.string V.defAttr (replicate 40 'X')])
  pure (pure (), never)

-- | A full-screen horizontal rule.
hRuleApp :: TestApp () ()
hRuleApp _cmd = do
  hRule singleBoxStyle
  pure (pure (), never)

----------------------------------------------------------------------------
-- Specs
----------------------------------------------------------------------------

spec :: Spec
spec = describe "Reflex.Vty.Widget.Box" $ do
  describe "alignText" $ do
    it "left-aligns by padding on the right" $ do
      alignText TextAlignment_Left "hi" '-' 5 `shouldBe` "hi---"

    it "right-aligns by padding on the left" $ do
      alignText TextAlignment_Right "hi" '-' 5 `shouldBe` "---hi"

    it "center-aligns with left bias for odd slack" $ do
      alignText TextAlignment_Center "hi" '-' 5 `shouldBe` "--hi-"

    it "center-aligns evenly for even slack" $ do
      alignText TextAlignment_Center "hi" '-' 6 `shouldBe` "--hi--"

    it "returns text unchanged when it fills the width" $ do
      alignText TextAlignment_Left "hello" '-' 3 `shouldBe` "hello"

    it "returns text unchanged when it exceeds the width" $ do
      alignText TextAlignment_Center "hello" '-' 2 `shouldBe` "hello"

    it "measures wide characters correctly" $ do
      alignText TextAlignment_Left "文" '-' 4 `shouldBe` "文--"

    it "handles empty text" $ do
      alignText TextAlignment_Center "" '-' 3 `shouldBe` "---"

  describe "centerText" $ do
    it "is a specialization of alignText" $ do
      centerText "hi" '-' 5 `shouldBe` alignText TextAlignment_Center "hi" '-' 5

  describe "boxStatic through the production host" $ do
    it "renders borders, inner padding, and the child content" $
      withApp defaultVtyAppConfig (12, 5) boxApp $ \dr -> do
        f0 <- lastFrame dr
        map (frameRow f0) [0 .. 4]
          `shouldBe` [ "┌──────────┐"
                     , "│hi        │"
                     , "│          │"
                     , "│          │"
                     , "└──────────┘"
                     ]
        gridWidth (frameGrid f0) `shouldBe` 12
        gridHeight (frameGrid f0) `shouldBe` 5
        stopApp dr

    it "handles 2-, 1-, 3-, and 0-cell dimensions" $
      withApp defaultVtyAppConfig (10, 4) boxApp $ \dr -> do
        f2 <- resizeApp dr (2, 2)
        frameRow f2 0 `shouldBe` "┌┐"
        frameRow f2 1 `shouldBe` "└┘"
        gridWidth (frameGrid f2) `shouldBe` 2
        f1 <- resizeApp dr (1, 2)
        frameRow f1 0 `shouldBe` " "
        frameRow f1 1 `shouldBe` " "
        f3 <- resizeApp dr (3, 1)
        frameRow f3 0 `shouldBe` " ─ "
        f0 <- resizeApp dr (0, 1)
        gridWidth (frameGrid f0) `shouldBe` 0
        stopApp dr

  describe "boxTitle through the production host" $ do
    it "renders all title alignments at a narrow width" $
      withApp defaultVtyAppConfig (8, 3) titleApp $ \dr -> do
        fCenter <- lastFrame dr
        frameRow fCenter 0 `shouldBe` "┌──Hi──┐"
        fLeft <- stepCommand dr TextAlignment_Left
        frameRow fLeft 0 `shouldBe` "┌Hi────┐"
        fRight <- stepCommand dr TextAlignment_Right
        frameRow fRight 0 `shouldBe` "┌────Hi┐"
        fCenterAgain <- stepCommand dr TextAlignment_Center
        frameRow fCenterAgain 0 `shouldBe` "┌──Hi──┐"
        stopApp dr

    it "clips a title wider than the box interior" $
      withApp defaultVtyAppConfig (8, 3) clipTitleApp $ \dr -> do
        f0 <- lastFrame dr
        frameRow f0 0 `shouldBe` "┌ABCDEF┐"
        stopApp dr

  describe "box through the production host" $ do
    it "translates clicks at screen coordinates to the inner coordinate system" $
      withApp defaultVtyAppConfig (10, 5) translateApp $ \dr -> do
        f0 <- lastFrame dr
        frameObservation f0 `shouldBe` (-1, -1)
        fClick <- stepInput dr (V.EvMouseDown 3 2 V.BLeft [])
        frameObservation fClick `shouldBe` (2, 1)
        fCorner <- stepInput dr (V.EvMouseDown 1 1 V.BLeft [])
        frameObservation fCorner `shouldBe` (0, 0)
        stopApp dr

    it "crops an oversized child image to the box interior" $
      withApp defaultVtyAppConfig (10, 4) clipApp $ \dr -> do
        f0 <- lastFrame dr
        frameRow f0 0 `shouldBe` "┌────────┐"
        frameRow f0 1 `shouldBe` "│XXXXXXXX│"
        frameRow f0 2 `shouldBe` "│        │"
        frameRow f0 3 `shouldBe` "└────────┘"
        gridWidth (frameGrid f0) `shouldBe` 10
        gridHeight (frameGrid f0) `shouldBe` 4
        stopApp dr

  describe "hRule through the production host" $ do
    it "fills its region and handles 2-, 1-, and 0-cell widths" $
      withApp defaultVtyAppConfig (6, 2) hRuleApp $ \dr -> do
        f0 <- lastFrame dr
        frameRow f0 0 `shouldBe` "──────"
        frameRow f0 1 `shouldBe` "──────"
        f2 <- resizeApp dr (2, 1)
        frameRow f2 0 `shouldBe` "──"
        f1 <- resizeApp dr (1, 1)
        frameRow f1 0 `shouldBe` "─"
        f0w <- resizeApp dr (0, 1)
        gridWidth (frameGrid f0w) `shouldBe` 0
        stopApp dr