reflex-vty-1.3.0.0: test/Reflex/Vty/Widget/SplitSpec.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Reflex.Vty.Widget.SplitSpec (spec) where
import qualified Data.Text as T
import qualified Graphics.Vty as V
import Reflex
import Test.Hspec hiding (focus)
import Reflex.Vty.Host
import Reflex.Vty.Test.AppHarness
import Reflex.Vty.Test.Snapshot (Grid (..), RenderSpan (..))
import Reflex.Vty.Widget
import Reflex.Vty.Widget.Split (splitH, splitV, splitVDrag)
import Reflex.Vty.Widget.Text (fill)
----------------------------------------------------------------------------
-- Helpers
----------------------------------------------------------------------------
-- | Text content of one row of a rendered grid.
rowText :: Grid -> Int -> String
rowText g y = T.unpack . T.concat . map renderSpanText $ gridRows g !! y
-- | All row texts of a grid.
rows :: Grid -> [String]
rows g = map (T.unpack . T.concat . map renderSpanText) (gridRows g)
-- | Index of the divider row (the only row made entirely of '-').
dividerRow :: Grid -> Int
dividerRow g = case [y | (y, r) <- zip [0 ..] (rows g), all (== '-') r && not (null r)] of
(y : _) -> y
[] -> error "no divider row found"
replicateRow :: Int -> Char -> String
replicateRow w c = replicate w c
mouseDownAt :: Int -> Int -> V.Event
mouseDownAt x y = V.EvMouseDown x y V.BLeft []
mouseUpAt :: Int -> Int -> V.Event
mouseUpAt x y = V.EvMouseUp x y (Just V.BLeft)
-- | Drag the divider from one row to another at a fixed column.
dragDivider :: AppDriver c o -> Int -> Int -> IO (Frame o)
dragDivider driver fromY toY = do
_ <- stepInput driver (mouseDownAt 0 fromY)
stepInput driver (mouseUpAt 0 toY)
----------------------------------------------------------------------------
-- Applications under test
----------------------------------------------------------------------------
-- | A draggable vertical split: pane A and pane B each fill their region
-- with their letter and report focus; the divider fills its row with '-'.
dragSplitApp :: TestApp () (T.Text, T.Text)
dragSplitApp _cmd = do
(a, b) <- splitVDrag (fill (pure '-')) (splitPane 'A') (splitPane 'B')
pure ((,) <$> a <*> b, never)
where
splitPane c = do
fill (pure c)
foc <- focus
pure (ffor foc $ \f -> T.pack [c, if f then '*' else '-'])
-- | A plain vertical split whose first pane takes a third of the height.
thirdSplitApp :: TestApp () ()
thirdSplitApp _cmd = do
_ <- splitV (pure (`div` 3)) (pure (True, True)) (fill (pure 'X')) (fill (pure 'Y'))
pure (pure (), never)
-- | A plain horizontal split whose first pane is 8 columns narrower
-- than the viewport.
hSplitApp :: TestApp () ()
hSplitApp _cmd = do
_ <- splitH (pure (subtract 8)) (pure (True, True)) (fill (pure 'P')) (fill (pure 'Q'))
pure (pure (), never)
-- | A vertical split with a nested horizontal split in the second pane.
nestedSplitApp :: TestApp () ()
nestedSplitApp _cmd = do
_ <-
splitV (pure (subtract 4)) (pure (True, True)) (fill (pure 'T')) $
splitH (pure (`div` 2)) (pure (True, True)) (fill (pure 'L')) (fill (pure 'R'))
pure (pure (), never)
----------------------------------------------------------------------------
-- Spec
----------------------------------------------------------------------------
spec :: Spec
spec = describe "Reflex.Vty.Widget.Split" $ do
describe "splitVDrag through the production host" $ do
it "starts with an even split, divider at half height" $
withApp defaultVtyAppConfig (20, 10) dragSplitApp $ \driver -> do
fs <- driverFrames driver
case reverse fs of
(f : _) -> do
rows (frameGrid f)
`shouldBe` replicate 5 (replicateRow 20 'A') ++ [replicateRow 20 '-'] ++ replicate 4 (replicateRow 20 'B')
dividerRow (frameGrid f) `shouldBe` 5
frameObservation f `shouldBe` ("A*", "B-")
[] -> expectationFailure "expected an initial frame"
stopApp driver
it "moves the divider on drag and keeps the fraction on resize" $
withApp defaultVtyAppConfig (20, 10) dragSplitApp $ \driver -> do
-- Press on the divider itself: nothing moves yet.
f1 <- stepInput driver (mouseDownAt 0 5)
dividerRow (frameGrid f1) `shouldBe` 5
-- Release on row 7: the divider moves to row 7.
f2 <- stepInput driver (mouseUpAt 0 7)
dividerRow (frameGrid f2) `shouldBe` 7
rows (frameGrid f2)
`shouldBe` replicate 7 (replicateRow 20 'A') ++ [replicateRow 20 '-'] ++ replicate 2 (replicateRow 20 'B')
frameObservation f2 `shouldBe` ("A*", "B-")
-- The user-chosen fraction (7/10) stays after a resize: 0.7 * 20 = 14.
resizeApp driver (20, 20)
fs <- driverFrames driver
case reverse fs of
(f3 : _) -> do
dividerRow (frameGrid f3) `shouldBe` 14
rows (frameGrid f3)
`shouldBe` replicate 14 (replicateRow 20 'A') ++ [replicateRow 20 '-'] ++ replicate 5 (replicateRow 20 'B')
[] -> expectationFailure "expected a frame after resize"
stopApp driver
it "supports repeated drags, each moving the divider again" $
withApp defaultVtyAppConfig (20, 10) dragSplitApp $ \driver -> do
f1 <- dragDivider driver 5 7
dividerRow (frameGrid f1) `shouldBe` 7
-- A second drag starts from the divider's new position.
f2 <- dragDivider driver 7 2
dividerRow (frameGrid f2) `shouldBe` 2
rows (frameGrid f2)
`shouldBe` replicate 2 (replicateRow 20 'A') ++ [replicateRow 20 '-'] ++ replicate 7 (replicateRow 20 'B')
-- And a third drag back down.
f3 <- dragDivider driver 2 9
dividerRow (frameGrid f3) `shouldBe` 9
stopApp driver
it "ignores drags that do not start on the divider" $
withApp defaultVtyAppConfig (20, 10) dragSplitApp $ \driver -> do
-- Press inside pane A (row 0) and release inside pane B's area.
f1 <- stepInput driver (mouseDownAt 0 0)
dividerRow (frameGrid f1) `shouldBe` 5
f2 <- stepInput driver (mouseUpAt 2 8)
dividerRow (frameGrid f2) `shouldBe` 5
rows (frameGrid f2)
`shouldBe` replicate 5 (replicateRow 20 'A') ++ [replicateRow 20 '-'] ++ replicate 4 (replicateRow 20 'B')
-- A click in pane A also leaves focus alone.
frameObservation f2 `shouldBe` ("A*", "B-")
stopApp driver
it "clamps drags past the bottom edge and keeps extents nonnegative" $
withApp defaultVtyAppConfig (20, 10) dragSplitApp $ \driver -> do
f <- dragDivider driver 5 50
let g = frameGrid f
-- The divider is clamped to the last row; pane B gets zero height.
dividerRow g `shouldBe` 9
rows g `shouldBe` replicate 9 (replicateRow 20 'A') ++ [replicateRow 20 '-']
-- The rendered grid itself never reports negative extents.
gridWidth g `shouldBe` 20
gridHeight g `shouldBe` 10
length (gridRows g) `shouldBe` 10
stopApp driver
it "switches pane focus by clicking the panes" $
withApp defaultVtyAppConfig (20, 10) dragSplitApp $ \driver -> do
fs0 <- driverFrames driver
case reverse fs0 of
(f0 : _) -> frameObservation f0 `shouldBe` ("A*", "B-")
[] -> expectationFailure "expected an initial frame"
-- Click inside pane B (rows 6..9): focus moves to B.
f1 <- stepInput driver (mouseDownAt 0 6)
frameObservation f1 `shouldBe` ("A-", "B*")
-- Release completes the click so pane A's mouse tracking resets.
_ <- stepInput driver (mouseUpAt 0 6)
-- Click inside pane A (rows 0..4): focus moves back.
f2 <- stepInput driver (mouseDownAt 0 0)
frameObservation f2 `shouldBe` ("A*", "B-")
_ <- stepInput driver (mouseUpAt 0 0)
stopApp driver
it "works at a one-row height and splits evenly after a resize" $
withApp defaultVtyAppConfig (20, 1) dragSplitApp $ \driver -> do
fs0 <- driverFrames driver
case reverse fs0 of
(f0 : _) -> do
-- The terminal has one row: the divider occupies that row.
rows (frameGrid f0) `shouldBe` [replicateRow 20 '-']
gridHeight (frameGrid f0) `shouldBe` 1
frameObservation f0 `shouldBe` ("A*", "B-")
[] -> expectationFailure "expected an initial frame"
-- No user fraction exists yet, so the grown terminal halves evenly.
resizeApp driver (20, 10)
fs <- driverFrames driver
case reverse fs of
(f1 : _) -> do
dividerRow (frameGrid f1) `shouldBe` 5
rows (frameGrid f1)
`shouldBe` replicate 5 (replicateRow 20 'A') ++ [replicateRow 20 '-'] ++ replicate 4 (replicateRow 20 'B')
[] -> expectationFailure "expected a frame after resize"
stopApp driver
describe "splitV" $ do
it "sizes the first pane with the given function and follows resizes" $
withApp defaultVtyAppConfig (20, 10) thirdSplitApp $ \driver -> do
fs <- driverFrames driver
case reverse fs of
(f : _) -> do
rows (frameGrid f)
`shouldBe` replicate 3 (replicateRow 20 'X') ++ replicate 7 (replicateRow 20 'Y')
[] -> expectationFailure "expected an initial frame"
-- 16 div 3 = 5 rows for X.
resizeApp driver (20, 16)
fs' <- driverFrames driver
case reverse fs' of
(f : _) ->
rows (frameGrid f)
`shouldBe` replicate 5 (replicateRow 20 'X') ++ replicate 11 (replicateRow 20 'Y')
[] -> expectationFailure "expected a frame after resize"
stopApp driver
describe "splitH" $ do
it "sizes the left pane with the given function" $
withApp defaultVtyAppConfig (20, 10) hSplitApp $ \driver -> do
fs <- driverFrames driver
case reverse fs of
(f : _) ->
rows (frameGrid f)
`shouldBe` replicate 10 (replicate 12 'P' ++ replicate 8 'Q')
[] -> expectationFailure "expected an initial frame"
stopApp driver
it "manages a nested split inside one pane" $
withApp defaultVtyAppConfig (20, 10) nestedSplitApp $ \driver -> do
fs <- driverFrames driver
case reverse fs of
(f : _) ->
rows (frameGrid f)
`shouldBe` replicate 6 (replicateRow 20 'T')
++ replicate 4 (replicate 10 'L' ++ replicate 10 'R')
[] -> expectationFailure "expected an initial frame"
stopApp driver