packages feed

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

module Cases.State
  ( runControlledInputsTest
  , runControlledStateTest
  , runHookStateTest
  , runCollectionApiTest
  ) where

import Control.Monad (forM_, when)
import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)
import Data.ByteString qualified as BS
import Data.IntMap.Strict qualified as IM
import Data.Text (Text)
import Data.Sequence qualified as Seq
import Data.Primitive.SmallArray qualified as SA
import NanoUI
import NanoUI.Context (Context (..), getStore, intKey, registerImages, lookupImageUv)
import NanoUI.Store (WidgetStore (..))
import NanoUI.Testing (clearDirty, collectTextSpans, isDirty, runFrame)
import NanoUI.Testing.Assert (assert, assertEq)
import NanoUI.Testing.Harness (keyInp, tabInp, warmup2, withInputOff)

runCollectionApiTest :: Context -> IORef Int -> IO ()
runCollectionApiTest ctx failed = do
  seen <- newIORef []
  _ <- runFrame ctx (withInputOff 300 100) $
    hstack (SA.smallArrayFromList [uiIO (modifyIORef' seen (key :)) | key <- [7, 2, 9 :: Int]])
  assertEq failed [9, 2, 7] =<< readIORef seen
  ((emptySelect, emptyRadio, combo), _, _, _) <- runFrame ctx (withInputOff 300 200) $
    withKey ("collection-options" :: Text) $ column $ do
      selectIndex <- select (SA.emptySmallArray :: SA.SmallArray Text) 5
      radioIndex <- radio (Seq.empty :: Seq.Seq Text) (-1)
      comboValue <- comboBox "Choose" (Seq.fromList ["Alpha", "Beta"]) "Beta"
      pure (selectIndex, radioIndex, comboValue)
  assertEq failed 0 emptySelect
  assertEq failed 0 emptyRadio
  assertEq failed "Beta" combo
  -- A failed image must not prevent later registrations in traversal order.
  ok <- registerImages ctx (Seq.fromList [(ImageId 0, 1, 1, BS.replicate 4 255), (ImageId 42, 1, 1, BS.replicate 4 255)])
  assertEq failed False ok
  registered <- lookupImageUv ctx (ImageId 42)
  assertEq failed True (case registered of Just _ -> True; Nothing -> False)

runControlledStateTest :: Context -> IORef Int -> IO ()
runControlledStateTest ctx failed = do
  callbacks <- newIORef []
  let
    inp = withInputOff 300 200
    ui checked text value = column $ do
      expectedId <- currentId
      (check, checked') <- checkbox' "Controlled" checked
      when (respChanged check) (uiIO (modifyIORef' callbacks (<> [checked'])))
      (field, _) <- textInput' text
      (range, _) <- slider' 0 100 value
      pure (expectedId, check, field, range)
  _ <- runFrame ctx inp (ui True "initial" 25)
  ((expectedId, check, field, range), _, _, _) <-
    runFrame ctx inp (ui False "replacement" 75)
  assertEq failed expectedId (respId check)
  store <- getStore ctx
  assertEq failed (Just 0) (IM.lookup (intKey (respId check)) (storeInt store))
  assertEq
    failed
    (Just "replacement")
    (IM.lookup (intKey (respId field)) (storeText store))
  assertEq failed (Just 75) (IM.lookup (intKey (respId range)) (storeFloat store))
  assertEq failed [] =<< readIORef callbacks

  -- Keyboard activation notifies the owner, which may decline the change.
  writeIORef (ctxFocusId ctx) (respId check)
  _ <-
    runFrame
      ctx
      (keyInp KeyEnter inp)
      (ui False "replacement" 75)
  assertEq failed [True] =<< readIORef callbacks
  _ <- runFrame ctx inp (ui False "replacement" 75)
  settled <- getStore ctx
  assertEq failed (Just 0) (IM.lookup (intKey (respId check)) (storeInt settled))
  assertEq failed [True] =<< readIORef callbacks

-- | Inputs show the value the caller passes: a value the caller changes
-- between frames is shown, a user edit the caller passes back is kept, and
-- one it ignores is undone on the next frame. 'runControlledStateTest' covers
-- a declined checkbox toggle.
runControlledInputsTest :: Context -> IORef Int -> IO ()
runControlledInputsTest ctx failed = do
  let
    inp = withInputOff 300 200
    ui (checked, text) = column $ do
      checked' <- checkbox "Opt" checked
      text' <- textInput text
      pure (checked', text')
  _ <- warmup2 ctx inp (ui (False, "one"))
  forM_
    [ -- The caller's new values, with no input.
      (inp, (True, "two"), (True, "two"))
    , -- Tab focuses the checkbox; Space toggles it and the caller keeps it.
      (tabInp inp, (True, "two"), (True, "two"))
    , (inp {inputChars = " "}, (True, "two"), (False, "two"))
    , (tabInp inp, (False, "two"), (False, "two"))
    , -- Tab moved focus to the field. A kept edit stays.
      (inp {inputChars = "x"}, (False, "two"), (False, "twox"))
    , (inp, (False, "twox"), (False, "twox"))
    , -- An ignored edit is undone on the following frame.
      (inp {inputChars = "y"}, (False, "twox"), (False, "twoxy"))
    , (inp, (False, "twox"), (False, "twox"))
    ]
    $ \(input, value, expected) -> do
      (result, _, _, _) <- runFrame ctx input (ui value)
      assertEq failed expected result
      spans <- collectTextSpans ctx
      assert failed (any (\(_, txt, _, _, _) -> txt == snd expected) spans)

runHookStateTest :: Context -> IORef Int -> IO ()
runHookStateTest ctx failed = do
  let
    inp = withInputOff 300 100
    check :: (Eq a, Show a) => Text -> NanoUI (a, a -> NanoUI ()) -> a -> a -> IO ()
    check key hook initial changed = do
      let
        evaluate = runNanoUI ctx inp (withKey key hook)
      (value, setValue) <- evaluate
      assertEq failed initial value
      clearDirty ctx
      runNanoUI ctx inp (setValue initial)
      assertEq failed False =<< isDirty ctx

      runNanoUI ctx inp (setValue changed)
      assertEq failed True =<< isDirty ctx
      assertEq failed changed . fst =<< evaluate

      -- Reuse the original setter: comparing against its captured initial
      -- value would incorrectly discard this update back to the initial.
      runNanoUI ctx inp (setValue initial)
      assertEq failed initial . fst =<< evaluate
  check "int" (useInt 0) 0 12
  check "float" (useFloat 0) 0 1.5
  check "text" (useText "initial") "initial" "changed"
  check "flag" (useFlag False) False True
  check "enum" (useEnum LT) LT GT
  check "dynamic" (useState (0 :: Int, False)) (0, False) (12, True)
  check "table-sort" (useTableSort (SortCol 0 SortAsc)) (SortCol 0 SortAsc) (SortCol 2 SortDesc)