{-| Module : Monomer.Widgets.Singles.NumericFieldSpec Copyright : (c) 2018 Francisco Vallarino License : BSD-3-Clause (see the LICENSE file) Maintainer : fjvallarino@gmail.com Stability : experimental Portability : non-portable Unit tests for NumericField widget. -} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} module Monomer.Widgets.Singles.NumericFieldSpec (spec) where import Control.Lens ((&), (^.), (.~)) import Control.Lens.TH (abbreviatedFields, makeLensesWith) import Data.Default import Data.Text (Text) import Test.Hspec import qualified Data.Sequence as Seq import Monomer.Core import Monomer.Core.Combinators import Monomer.Event import Monomer.TestUtil import Monomer.TestEventUtil import Monomer.Widgets.Containers.Stack import Monomer.Widgets.Singles.Button import Monomer.Widgets.Singles.NumericField import qualified Monomer.Lens as L data TestEvt = IntegralChanged Int | FractionalChanged (Maybe Double) | ValidNumber Bool | GotFocus Path | LostFocus Path deriving (Eq, Show) data IntegralModel = IntegralModel { _imIntegralValue :: Int, _imIntegralValid :: Bool } deriving (Eq, Show) data FractionalModel = FractionalModel { _fmFractionalValue :: Maybe Double, _fmFractionalValid :: Bool } deriving (Eq, Show) makeLensesWith abbreviatedFields ''IntegralModel makeLensesWith abbreviatedFields ''FractionalModel spec :: Spec spec = describe "NumericField" $ do specIntegral specFractional specIntegral :: Spec specIntegral = describe "IntegralField" $ do handleEventIntegral handleEventValueIntegral handleEventMouseDragIntegral handleEventReadOnly getSizeReqIntegral testIntegralWidgetType handleEventIntegral :: Spec handleEventIntegral = describe "handleEventIntegral" $ do it "should remove the contents and get Nothing as model value" $ do modelBasic [evtKG keyA, evtK keyBackspace] ^. integralValue `shouldBe` 0 modelBasic [evtKG keyA, evtK keyBackspace] ^. integralValid `shouldBe` False it "should input '123' without select on focus" $ do modelBasic [evtT "1", evtT "2", evtT "3"] ^. integralValue `shouldBe` 1230 modelBasic [evtT "1", evtT "2", evtT "3"] ^. integralValid `shouldBe` True it "should input '1'" $ do model [evtT "1"] ^. integralValue `shouldBe` 1 model [evtT "1"] ^. integralValid `shouldBe` True events [evtT "1"] `shouldBe` Seq.fromList [ValidNumber True] it "should input '-1'" $ do model [evtT "-1"] ^. integralValue `shouldBe` -1 model [evtT "-1"] ^. integralValid `shouldBe` True events [evtT "-1"] `shouldBe` Seq.fromList [ValidNumber True] it "should input '1501'" $ do model [evtT "1", evtT "5", evtT "0", evtT "1"] ^. integralValue `shouldBe` 1501 model [evtT "1", evtT "5", evtT "0", evtT "1"] ^. integralValid `shouldBe` True events [evtT "1", evtT "5", evtT "0", evtT "1"] `shouldBe` Seq.fromList (replicate 4 (ValidNumber True)) it "should input '1502', but fail because of maxValue" $ do model [evtT "1", evtT "5", evtT "0", evtT "2"] ^. integralValue `shouldBe` 150 model [evtT "1", evtT "5", evtT "0", evtT "2"] ^. integralValid `shouldBe` False events [evtT "1", evtT "5", evtT "0", evtT "2"] `shouldBe` Seq.fromList (replicate 3 (ValidNumber True) ++ [ValidNumber False]) it "should input '123', remove one character and input '4'" $ do model [evtT "123", delCharL, evtT "4"] ^. integralValue `shouldBe` 124 model [evtT "123", delCharL, evtT "4"] ^. integralValid `shouldBe` True events [evtT "123", delCharL, evtT "4"] `shouldBe` Seq.fromList (replicate 3 (ValidNumber True)) it "should input '123', remove one word and input '456'" $ do model [evtT "123", delWordL, evtT "456"] ^. integralValue `shouldBe` 456 model [evtT "123", delWordL, evtT "456"] ^. integralValid `shouldBe` True events [evtT "123", delWordL, evtT "456"] `shouldBe` Seq.fromList [ValidNumber True, ValidNumber False, ValidNumber True] it "should update the model when using the wheel" $ do let p = Point 100 10 let steps1 = [WheelScroll p (Point 0 (-64)) WheelNormal] let steps2 = [WheelScroll p (Point 0 300) WheelFlipped] let steps3 = [WheelScroll p (Point 0 100) WheelNormal] let steps4 = [WheelScroll p (Point 0 2000) WheelNormal] model steps1 ^. integralValue `shouldBe` -64 model steps2 ^. integralValue `shouldBe` -200 model steps3 ^. integralValue `shouldBe` 100 model steps4 ^. integralValue `shouldBe` 1501 it "should generate IgnoreParentEvents when using the wheel" $ do let p = Point 100 10 let steps = [WheelScroll p (Point 0 (-64)) WheelNormal] reqs steps `shouldSatisfy` (IgnoreParentEvents `elem`) it "should generate an event when focus is received" $ events [evtFocus] `shouldBe` Seq.singleton (GotFocus emptyPath) it "should generate an event when focus is lost" $ events [evtBlur] `shouldBe` Seq.singleton (LostFocus emptyPath) where wenv = mockWenv (IntegralModel 0 True) basicIntNode :: WidgetNode IntegralModel TestEvt basicIntNode = numericField_ integralValue [validInput integralValid, selectOnFocus_ False] intCfg = [minValue (-200), maxValue 1501, validInput integralValid, validInputV ValidNumber, onFocus GotFocus, onBlur LostFocus] intNode = numericField_ integralValue intCfg model es = nodeHandleEventModel wenv (evtFocus : es) intNode modelBasic es = nodeHandleEventModel wenv es basicIntNode events es = nodeHandleEventEvts wenv es intNode reqs es = nodeHandleEventReqs wenv es intNode handleEventValueIntegral :: Spec handleEventValueIntegral = describe "handleEventIntegral" $ do it "should input an '10'" $ evts [evtT "1", evtT "0"] `shouldBe` Seq.fromList [IntegralChanged 1, IntegralChanged 10] it "should input '1', move to beginning and input '5'" $ do let steps = [evtT "1", moveLineL, evtT "5"] lastEvt steps `shouldBe` IntegralChanged 51 it "should input '1', input '.' then input '5'" $ do let steps = [evtT "1", evtT ".", evtT "5"] lastEvt steps `shouldBe` IntegralChanged 15 model steps ^. integralValid `shouldBe` True it "should input '3', input 'a' then input '6'" $ do let steps = [evtT "3", evtT "a", evtT "6"] lastEvt steps `shouldBe` IntegralChanged 36 model steps ^. integralValid `shouldBe` True it "should input '1234', delete line then input '777'" $ do let steps = [evtT "1234", selLineL, evtT "777"] lastEvt steps `shouldBe` IntegralChanged 777 model steps ^. integralValid `shouldBe` True where wenv = mockWenv (IntegralModel 0 False) intNode = numericFieldV_ 0 IntegralChanged [maxValue 2345, selectOnFocus, validInput integralValid] evts es = nodeHandleEventEvts wenv (evtFocus : es) intNode model es = nodeHandleEventModel wenv (evtFocus : es) intNode lastIdx es = Seq.index es (Seq.length es - 1) lastEvt es = lastIdx (evts es) handleEventMouseDragIntegral :: Spec handleEventMouseDragIntegral = describe "handleEventMouseDragIntegral" $ do it "should drag upwards 100 pixels, setting the value to 100" $ do let selStart = Point 50 30 let selEnd = Point 50 (-70) let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. integralValue `shouldBe` 100 it "should drag downwards 100 pixels, setting the value to -200 (dragRate = 2)" $ do let selStart = Point 50 50 let selEnd = Point 50 150 let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. integralValue `shouldBe` -200 it "should drag downwards 1000 pixels, staying at -500 (the minimum)" $ do let selStart = Point 50 50 let selEnd = Point 50 1050 let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. integralValue `shouldBe` -500 it "should drag upwnwards 1000 pixels, staying at 500 (the maximum)" $ do let selStart = Point 50 50 let selEnd = Point 50 (-950) let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. integralValue `shouldBe` 500 it "should drag downwards 30 and 20 pixels, setting the value to -5" $ do let selStart = Point 50 30 let selMid = Point 50 60 let selEnd = Point 50 50 let steps = [ evtPress selStart, evtMove selMid, evtRelease selMid, evtPress selStart, evtMove selEnd, evtRelease selEnd ] model steps ^. integralValue `shouldBe` -50 it "should set focus and drag upwards 100 pixels, but value stay at 0 since shift is not pressed" $ do let selStart = Point 50 30 let selEnd = Point 50 (-70) let steps = [evtK keyTab, evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. integralValue `shouldBe` 0 it "should set focus and drag upwards 100 pixels, setting the value to 100 since shift is pressed" $ do let selStart = Point 50 30 let selEnd = Point 50 (-70) let steps = [evtKS keyTab, evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. integralValue `shouldBe` 100 it "should drag upwards 100 pixels, setting the value to 100 even if it was double clicked on" $ do let selStart = Point 50 30 let selEnd = Point 50 (-70) let steps = [evtDblClick selStart, evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. integralValue `shouldBe` 100 where wenv = mockWenv (IntegralModel 0 False) & L.inputStatus . L.keyMod . L.leftShift .~ True integralNode = vstack [ button "Test" (IntegralChanged 0), -- Used only to have focus numericField integralValue, numericField_ integralValue [dragRate 2, minValue (-500), maxValue 500] ] evts es = nodeHandleEventEvts wenv es integralNode model es = nodeHandleEventModel wenv es integralNode lastIdx es = Seq.index es (Seq.length es - 1) lastEvt es = lastIdx (evts es) getSizeReqIntegral :: Spec getSizeReqIntegral = describe "getSizeReqIntegral" $ do it "should return width = Flex 50 1" $ sizeReqW `shouldBe` expandSize 50 1 it "should return height = Fixed 20" $ sizeReqH `shouldBe` fixedSize 20 it "should return width = Flex 110 1 when resizeOnChange = True" $ sizeReqW2 `shouldBe` expandSize 110 1 it "should return height = Fixed 20 when resizeOnChange = True" $ sizeReqH2 `shouldBe` fixedSize 20 where wenv = mockWenvEvtUnit (IntegralModel 10000000000 True) (sizeReqW, sizeReqH) = nodeGetSizeReq wenv (numericField integralValue) numericResize = numericField_ integralValue [resizeOnChange] (sizeReqW2, sizeReqH2) = nodeGetSizeReq wenv numericResize testIntegralWidgetType :: Spec testIntegralWidgetType = describe "testIntegralWidgetType" $ do it "should set the correct widgetType" $ node ^. L.info . L.widgetType `shouldBe` "numericField-Int" where node :: WidgetNode IntegralModel TestEvt node = numericField integralValue -- ------------------------------ -- Fractional field -- ------------------------------ specFractional :: Spec specFractional = describe "FractionalField" $ do handleEventFractional handleEventValueFractional handleEventMouseDragFractional handleShiftFocusFractional getSizeReqFractional testFractionalWidgetType handleEventFractional :: Spec handleEventFractional = describe "handleEventFractional" $ do it "should remove the contents and get Nothing as model value" $ do modelBasic [evtKG keyA, evtK keyBackspace] ^. fractionalValue `shouldBe` Nothing modelBasic [evtKG keyA, evtK keyBackspace] ^. fractionalValid `shouldBe` True it "should input '123' without select on focus" $ do modelBasic [evtT "1", evtT "2", evtT "3"] ^. fractionalValue `shouldBe` Just 1230 modelBasic [evtT "1", evtT "2", evtT "3"] ^. fractionalValid `shouldBe` True it "should input '1.23'" $ do model [evtT "1.23"] ^. fractionalValue `shouldBe` Just 1.23 model [evtT "1.23"] ^. fractionalValid `shouldBe` True it "should input '-1'" $ do model [evtT "-1"] ^. fractionalValue `shouldBe` Just (-1) model [evtT "-1"] ^. fractionalValid `shouldBe` True it "should input '1501'" $ do model [evtT "1", evtT "5", evtT "0", evtT "1"] ^. fractionalValue `shouldBe` Just 1501 model [evtT "1", evtT "5", evtT "0", evtT "1"] ^. fractionalValid `shouldBe` True it "should input '1502', but fail because of maxValue" $ do model [evtT "1", evtT "5", evtT "0", evtT "2"] ^. fractionalValue `shouldBe` Just 150 model [evtT "1", evtT "5", evtT "0", evtT "2"] ^. fractionalValid `shouldBe` False it "should input '123', remove one character and input '4'" $ do model [evtT "123", delCharL, evtT "4"] ^. fractionalValue `shouldBe` Just 124 model [evtT "123", delCharL, evtT "4"] ^. fractionalValid `shouldBe` True it "should input '123', remove one word and input '456'" $ do model [evtT "123", delWordL, evtT "456"] ^. fractionalValue `shouldBe` Just 456 model [evtT "123", delWordL, evtT "456"] ^. fractionalValid `shouldBe` True it "should input '123.34', remove one word and input '56'" $ do model [evtT "123.34", delWordL, evtT "56"] ^. fractionalValue `shouldBe` Just 123.56 model [evtT "123.34", delWordL, evtT "56"] ^. fractionalValid `shouldBe` True it "should input '123.34', remove two words and input '56'" $ do model [evtT "123.34", delWordL, delWordL, evtT "56"] ^. fractionalValue `shouldBe` Just 56 model [evtT "123.34", delWordL, delWordL, evtT "56"] ^. fractionalValid `shouldBe` True it "should update the model when using the wheel" $ do let p = Point 100 10 let steps1 = [WheelScroll p (Point 0 (-8000)) WheelNormal] let steps2 = [WheelScroll p (Point 0 360) WheelFlipped] let steps3 = [WheelScroll p (Point 0 8700) WheelNormal] let steps4 = [WheelScroll p (Point 0 16000) WheelNormal] model steps1 ^. fractionalValue `shouldBe` Just (-500) model steps2 ^. fractionalValue `shouldBe` Just (-36) model steps3 ^. fractionalValue `shouldBe` Just 870 model steps4 ^. fractionalValue `shouldBe` Just 1501 it "should generate IgnoreParentEvents when using the wheel" $ do let p = Point 100 10 let steps = [WheelScroll p (Point 0 (-64)) WheelNormal] reqs steps `shouldSatisfy` (IgnoreParentEvents `elem`) it "should generate an event when focus is received" $ events evtFocus `shouldBe` Seq.singleton (GotFocus emptyPath) it "should generate an event when focus is lost" $ events evtBlur `shouldBe` Seq.singleton (LostFocus emptyPath) where wenv = mockWenv (FractionalModel (Just 0) True) basicFractionalNode :: WidgetNode FractionalModel TestEvt basicFractionalNode = numericField_ fractionalValue [selectOnFocus_ False, validInput fractionalValid] floatCfg = [minValue (Just (-500)), maxValue (Just 1501), validInput fractionalValid, onFocus GotFocus, onBlur LostFocus] floatNode = numericField_ fractionalValue floatCfg model es = nodeHandleEventModel wenv es floatNode modelBasic es = nodeHandleEventModel wenv es basicFractionalNode events evt = nodeHandleEventEvts wenv [evt] floatNode reqs es = nodeHandleEventReqs wenv es floatNode handleEventValueFractional :: Spec handleEventValueFractional = describe "handleEventValueFractional" $ do it "should input an '100'" $ evts [evtT "1", evtT "0", evtT "0"] `shouldBe` Seq.fromList [FractionalChanged (Just 10), FractionalChanged (Just 100)] it "should input a '1' and be considered invalid" $ do evts [evtT "1"] `shouldBe` Seq.fromList [] model [evtT "1"] ^. fractionalValid `shouldBe` False it "should input '1', move to beginning and input '5'" $ do let steps = [evtT "1", moveLineL, evtT "5"] lastEvt steps `shouldBe` FractionalChanged (Just 51) it "should input '1', input '.' then input '5'" $ do let steps = [evtT "10", evtT ".", evtT "5"] lastEvt steps `shouldBe` FractionalChanged (Just 10.5) model steps ^. fractionalValid `shouldBe` True it "should input '20', input '.' twice then input '777'" $ do let steps = [evtT "20", evtT ".", evtT ".", evtT "7", evtT "7", evtT "7"] lastEvt steps `shouldBe` FractionalChanged (Just 20.77) model steps ^. fractionalValid `shouldBe` True it "should input '10', '.' then input '2345'" $ do let steps = [evtT "10", evtT ".", evtT "2", evtT "3", evtT "4", evtT "5"] lastEvtDecimals steps `shouldBe` FractionalChanged (Just 10.234) it "should input '3', input 'a' then input '6'" $ do let steps = [evtT "3", evtT "a", evtT "6"] lastEvt steps `shouldBe` FractionalChanged (Just 36) model steps ^. fractionalValid `shouldBe` True it "should input '1234', delete line then input '777'" $ do let steps = [evtT "1234", selLineL, evtT "777"] lastEvt steps `shouldBe` FractionalChanged (Just 777) model steps ^. fractionalValid `shouldBe` True where wenv = mockWenv (FractionalModel (Just 0) False) floatNode = numericFieldV_ (Just 0) FractionalChanged [minValue (Just 10), maxValue (Just 2345), selectOnFocus, validInput fractionalValid] floatDecimalsNode = numericFieldV_ (Just 0) FractionalChanged [selectOnFocus, decimals 3] evts es = nodeHandleEventEvts wenv (evtFocus : es) floatNode evtsAlt es = nodeHandleEventEvts wenv (evtFocus : es) floatDecimalsNode model es = nodeHandleEventModel wenv (evtFocus : es) floatNode lastIdx es = Seq.index es (Seq.length es - 1) lastEvt es = lastIdx (evts es) lastEvtDecimals es = lastIdx (evtsAlt es) handleEventMouseDragFractional :: Spec handleEventMouseDragFractional = describe "handleEventMouseDragFractional" $ do it "should drag upwards 100 pixels, setting the value to 10" $ do let selStart = Point 50 30 let selEnd = Point 50 (-70) let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. fractionalValue `shouldBe` Just 10 it "should drag downwards 100 pixels, setting the value to -20 (dragRate = 0.2)" $ do let selStart = Point 50 50 let selEnd = Point 50 150 let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. fractionalValue `shouldBe` Just (-20) it "should drag downwards 30 and 20 pixels, setting the value to -5" $ do let selStart = Point 50 30 let selMid = Point 50 60 let selEnd = Point 50 50 let steps = [ evtPress selStart, evtMove selMid, evtRelease selMid, evtPress selStart, evtMove selEnd, evtRelease selEnd ] model steps ^. fractionalValue `shouldBe` Just (-5) it "should drag upwards 100 pixels, but value stay at 0 since shift is not pressed" $ do let selStart = Point 50 30 let selEnd = Point 50 (-70) let steps = [evtK keyTab, evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. fractionalValue `shouldBe` Just 0 it "should drag upwards 100 pixels, setting the value to 10 since shift not pressed" $ do let selStart = Point 50 30 let selEnd = Point 50 (-70) let steps = [evtKS keyTab, evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. fractionalValue `shouldBe` Just 10 it "should drag upwards 100 pixels, setting the value to 10 even if it was double clicked on" $ do let selStart = Point 50 30 let selEnd = Point 50 (-70) let steps = [evtDblClick selStart, evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. fractionalValue `shouldBe` Just 10 where wenv = mockWenv (FractionalModel (Just 0) False) & L.inputStatus . L.keyMod . L.leftShift .~ True floatNode = vstack [ button "Test" (FractionalChanged (Just 0)), -- Used only to have focus numericField fractionalValue, numericField_ fractionalValue [dragRate 0.2] ] evts es = nodeHandleEventEvts wenv es floatNode model es = nodeHandleEventModel wenv es floatNode lastIdx es = Seq.index es (Seq.length es - 1) lastEvt es = lastIdx (evts es) handleShiftFocusFractional :: Spec handleShiftFocusFractional = describe "handleShiftFocusFractional" $ do it "should set focus when clicked" $ do evts [evtPress p] `shouldBe` Seq.fromList [GotFocus $ Seq.fromList [0, 0]] it "should not set focus when clicked with shift pressed" $ do evts [evtKS keyA, evtPress p] `shouldBe` Seq.empty where wenv = mockWenv (FractionalModel (Just 0) False) p = Point 100 30 floatNode = vstack [ numericField_ fractionalValue [wheelRate 1], numericField_ fractionalValue [wheelRate 1, onFocus GotFocus] ] evts es = nodeHandleEventEvts wenv es floatNode handleEventReadOnly :: Spec handleEventReadOnly = describe "handleEventReadOnly" $ do it "should ignore text input" $ do let steps = [moveCharR, delCharL, evtT "5"] model steps ^. fractionalValue `shouldBe` initValue it "should ignore drag" $ do let selStart = Point 5 5 let selEnd = Point 100 2000 let steps = [evtPress selStart, evtMove selEnd, evtRelease selEnd] model steps ^. fractionalValue `shouldBe` initValue it "should ignore wheel" $ do let steps = [WheelScroll (Point 100 10) (Point 0 (-2000)) WheelNormal] model steps ^. fractionalValue `shouldBe` initValue where initValue = Just 42.4 wenv = mockWenv (FractionalModel initValue False) & L.inputStatus . L.keyMod . L.leftShift .~ True fieldCfg = [readOnly :: NumericFieldCfg FractionalModel TestEvt (Maybe Double)] fieldNode = numericField_ fractionalValue fieldCfg model es = nodeHandleEventModel wenv es fieldNode getSizeReqFractional :: Spec getSizeReqFractional = describe "getSizeReqFractional" $ do it "should return width = Flex 70 1" $ sizeReqW `shouldBe` expandSize 70 1 it "should return height = Fixed 20" $ sizeReqH `shouldBe` fixedSize 20 it "should return width = Flex 110 1 when resizeOnChange = True" $ sizeReqW2 `shouldBe` expandSize 110 1 it "should return height = Fixed 20 when resizeOnChange = True" $ sizeReqH2 `shouldBe` fixedSize 20 where wenv = mockWenvEvtUnit (FractionalModel (Just 10000000) True) (sizeReqW, sizeReqH) = nodeGetSizeReq wenv (numericField fractionalValue) numericResize = numericField_ fractionalValue [resizeOnChange] (sizeReqW2, sizeReqH2) = nodeGetSizeReq wenv numericResize testFractionalWidgetType :: Spec testFractionalWidgetType = describe "testFractionalWidgetType" $ do it "should set the correct widgetType" $ node ^. L.info . L.widgetType `shouldBe` "numericField-Maybe Double" where node :: WidgetNode FractionalModel TestEvt node = numericField fractionalValue