packages feed

nano-ui-0.1.0.0: test/integration/Cases/Keyboard.hs

module Cases.Keyboard
  ( runKeyboardButtonTest
  , runKeyboardCheckboxTest
  , runKeyboardSliderTest
  , runKeyboardRadioTest
  , runKeyboardToggleTest
  , runKeyboardTabHeaderTest
  , runKeyboardDisabledTest
  , runKeyboardModalEligibilityTest
  , runKeyboardFocusRingTest
  ) where

import Data.IORef (IORef, newIORef, writeIORef)
import Data.IntMap.Strict qualified as IM
import NanoUI
import NanoUI.Context (Context (..), getFocusVisible, intKey)
import NanoUI.Emit qualified as Emit
import NanoUI.Store (WidgetStore (..))
import NanoUI.Testing
import NanoUI.Testing.Assert (assert, assertEq, assertGt)
import NanoUI.Testing.Harness (centerOf, clickPair, held, keyInp, tabInp, warmup2, withInputOff)

-- Retaining focus while a widget becomes disabled must not bypass the same
-- guard used by pointer interaction. Exercise the shared key-navigation hook.
runKeyboardDisabledTest :: Context -> IORef Int -> IO ()
runKeyboardDisabledTest _ctx failed = do
  let inp = withInputOff 300 160
      check :: (Eq a, Show a) => NanoUI (Response, a) -> Input -> IO ()
      check widget pressed = do
        ctx <- newContext
        ((resp, before), _, _, _) <- runFrame ctx inp widget
        let wid = respId resp
        st <- getStore ctx
        writeIORef (ctxFocusId ctx) wid
        ((afterResp, after), _, _, _) <- runFrame ctx pressed (disabledWhen True widget)
        assertEq failed after before
        assert failed (not (respChanged afterResp) && not (respClicked afterResp))
        afterStore <- getStore ctx
        assertEq failed
          (IM.lookup (intKey wid) (storeText st))
          (IM.lookup (intKey wid) (storeText afterStore))
  check (checkbox' "Disabled" False) (keyInp KeyEnter inp)
  check (checkbox' "Disabled" False) (spaceInp inp)
  check (slider' 0 100 50) (keyInp KeyRight inp)
  check (toggleSwitch' False) (spaceInp inp)
  check (textInput' "initial") (inp {inputChars = "x"})
  check (textArea' "initial") (inp {inputChars = "x"})
  check (searchField' "Search" "initial") (inp {inputChars = "x"})
  check (comboBox' "Choose" ["initial", "other"] "initial") (inp {inputChars = "x"})
  check (do r <- button' "Disabled"; pure (r, respClicked r)) (keyInp KeyEnter inp)

runKeyboardModalEligibilityTest :: Context -> IORef Int -> IO ()
runKeyboardModalEligibilityTest ctx failed = do
  let inp = withInputOff 400 300
      ui = column $ do
        outside <- checkbox' "Outside" False
        (_, inside) <- modal True "Modal" (checkbox' "Inside" False)
        pure (outside, inside)
  ((outside, inside), _, _, _) <- runFrame ctx inp ui
  writeIORef (ctxFocusId ctx) (respId (fst outside))
  (((_, outsideValue), _), _, _, _) <- runFrame ctx (keyInp KeyEnter inp) ui
  assert failed (not outsideValue)
  case inside of
    Nothing -> assert failed False
    Just (resp, _) -> do
      writeIORef (ctxFocusId ctx) (respId resp)
      ((_, after), _, _, _) <- runFrame ctx (keyInp KeyEnter inp) ui
      assert failed (maybe False snd after)

-- | A space key-down frame (space arrives as a character, not a Key).
spaceInp :: Input -> Input
spaceInp inp = inp {inputChars = " "}

-- | Plain buttons activate with Enter and Space while focused.
runKeyboardButtonTest :: Context -> IORef Int -> IO ()
runKeyboardButtonTest ctx failed = do
  let inp0 = withInputOff 200 120
      ui = column $ do
        a <- button "A"
        b <- button "B"
        pure (a, b)
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  ((aEnter, _), _, _, _) <- runFrame ctx (keyInp KeyEnter inp0) ui
  assert failed aEnter
  ((aSpace, _), _, _, _) <- runFrame ctx (spaceInp inp0) ui
  assert failed aSpace
  _ <- runFrame ctx (tabInp inp0) ui
  ((_, bEnter), _, _, _) <- runFrame ctx (keyInp KeyEnter inp0) ui
  assert failed bEnter

-- | A focused checkbox toggles with Space and Enter, and 'Emit.checkbox'
-- emits its new value on keyboard activation.
runKeyboardCheckboxTest :: Context -> IORef Int -> IO ()
runKeyboardCheckboxTest ctx failed = do
  checkedRef <- newIORef False
  let inp0 = withInputOff 200 100
      ui = column (held checkedRef (checkbox' "Opt"))
  _ <- warmup2 ctx inp0 ui
  _ <- runFrame ctx (tabInp inp0) ui
  ((_, checked1), _, _, _) <- runFrame ctx (spaceInp inp0) ui
  assert failed checked1
  ((_, checked2), _, _, _) <- runFrame ctx (keyInp KeyEnter inp0) ui
  assert failed (not checked2)
  let emitUi = do
        wid <- currentId
        Emit.checkbox "Emit" False id
        pure wid
  (wid, _, _, _) <- runFrame ctx inp0 emitUi
  writeIORef (ctxFocusId ctx) wid
  (_, messages, _, _) <- runFrame ctx (keyInp KeyEnter inp0) emitUi
  assertEq failed [True] (decodeMessages messages :: [Bool])

-- | A focused slider steps with the arrow keys.
runKeyboardSliderTest :: Context -> IORef Int -> IO ()
runKeyboardSliderTest ctx failed = do
  valueRef <- newIORef 50
  let inp0 = withInputOff 300 80
      ui = column (held valueRef (slider' 0 100))
  (_, v0) <- warmup2 ctx inp0 ui
  assertEq failed v0 50
  _ <- runFrame ctx (tabInp inp0) ui
  ((_, v1), _, _, _) <- runFrame ctx (keyInp KeyRight inp0) ui
  assertGt failed v1 50
  ((_, v2), _, _, _) <- runFrame ctx (keyInp KeyLeft inp0) ui
  assertEq failed v2 50
  ((_, v3), _, _, _) <- runFrame ctx (keyInp KeyDown inp0) ui
  assertEq failed v3 49
  ((_, v4), _, _, _) <- runFrame ctx (keyInp KeyUp inp0) ui
  assertEq failed v4 50

-- | A focused radio group changes selection with the arrow keys.
runKeyboardRadioTest :: Context -> IORef Int -> IO ()
runKeyboardRadioTest ctx failed = do
  selectedRef <- newIORef 0
  let inp0 = withInputOff 200 160
      ui = column (held selectedRef (radio' ["A", "B", "C"]))
  (_, sel0) <- warmup2 ctx inp0 ui
  assertEq failed sel0 0
  _ <- runFrame ctx (tabInp inp0) ui
  ((_, sel1), _, _, _) <- runFrame ctx (keyInp KeyDown inp0) ui
  assertEq failed sel1 1
  ((_, sel2), _, _, _) <- runFrame ctx (keyInp KeyDown inp0) ui
  assertEq failed sel2 2
  ((_, sel3), _, _, _) <- runFrame ctx (keyInp KeyUp inp0) ui
  assertEq failed sel3 1

-- | A toggle switch flips with Space and Enter while focused, and on click.
runKeyboardToggleTest :: Context -> IORef Int -> IO ()
runKeyboardToggleTest ctx failed = do
  onRef <- newIORef False
  let inp0 = withInputOff 200 100
      ui = column (held onRef toggleSwitch')
  (resp0, v0) <- warmup2 ctx inp0 ui
  assert failed (not v0)
  _ <- runFrame ctx (tabInp inp0) ui
  ((_, v1), _, _, _) <- runFrame ctx (spaceInp inp0) ui
  assert failed v1
  ((_, v2), _, _, _) <- runFrame ctx (keyInp KeyEnter inp0) ui
  assert failed (not v2)
  let (press, release) = clickPair inp0 (centerOf resp0)
  _ <- runFrame ctx press ui
  ((clicked, v3), _, _, _) <- runFrame ctx release ui
  assert failed (respClicked clicked && v3)
  _ <- runFrame ctx press ui
  ((clicked2, v4), _, _, _) <- runFrame ctx release ui
  assert failed (respClicked clicked2 && not v4)

data KB = KBA | KBB
  deriving (Eq, Show)

-- | Tab headers are focusable and switch the active tab with Enter.
runKeyboardTabHeaderTest :: Context -> IORef Int -> IO ()
runKeyboardTabHeaderTest ctx failed = do
  let inp0 = withInputOff 300 100
      ui cur =
        tabs cur
          [ tab KBA "Alpha" (label "BodyA")
          , tab KBB "Beta" (label "BodyB")
          ]
  _ <- warmup2 ctx inp0 (ui KBA)
  _ <- runFrame ctx (tabInp inp0) (ui KBA)
  (active1, _, _, _) <- runFrame ctx (keyInp KeyEnter inp0) (ui KBA)
  assertEq failed active1 KBA
  _ <- runFrame ctx (tabInp inp0) (ui KBA)
  (active2, _, _, _) <- runFrame ctx (keyInp KeyEnter inp0) (ui KBA)
  assertEq failed active2 KBB

-- Moving focus with Tab shows the focus ring; a pointer press hides it.
runKeyboardFocusRingTest :: Context -> IORef Int -> IO ()
runKeyboardFocusRingTest ctx failed = do
  let inp = withInputOff 300 160
      ui = column (button' "Go")
  _ <- warmup2 ctx inp ui
  assert failed . not =<< getFocusVisible ctx
  _ <- runFrame ctx (tabInp inp) ui
  assert failed =<< getFocusVisible ctx
  let (press, release) = clickPair inp (V2 5 150)
  _ <- runFrame ctx press ui
  _ <- runFrame ctx release ui
  assert failed . not =<< getFocusVisible ctx