{-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecursiveDo #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- Notice to those working on this test suite: -- It doesn't appear to be possible to use --match to run a particular test, in -- this situation the test may hang before reaching our code. This seems to -- happen when the test is not the first test in the block. As a workaround, you -- can just comment out the other tests. Also, using `xit` will cause the same -- issue. `xit` tests must be the last tests in the session, or the session will -- hang in the following test. import Prelude hiding (fail) import Control.Concurrent import qualified Control.Concurrent.Async as Async import Control.Lens.Operators import Control.Monad hiding (fail) import Control.Monad.Catch import Control.Monad.Fail import Control.Monad.Fix import Control.Monad.IO.Class import Control.Monad.Ref import Data.Constraint.Extras import Data.Constraint.Extras.TH import Data.Dependent.Map (DMap) import Data.Dependent.Sum (DSum(..), (==>)) import Data.Functor.Identity import Data.Functor.Misc import Data.GADT.Compare.TH import Data.GADT.Show.TH import Data.IORef (IORef) import Data.List (sort) import Data.Maybe import Data.Proxy import Data.Text (Text) import Language.Javascript.JSaddle (syncPoint, liftJSM) import Language.Javascript.JSaddle.Warp import Network.HTTP.Types (status200) import Network.Socket import Network.Wai import Network.WebSockets import Reflex.Dom.Core import Reflex.Dom.Widget.Input (dropdown) import Reflex.Patch.DMapWithMove import System.Directory import System.Environment import System.IO (stderr) import System.IO.Silently import System.IO.Temp import System.Process import System.Which (staticWhich) import qualified Test.HUnit as HUnit import qualified Test.Hspec as H import qualified Test.Hspec.Core.Spec as H import Test.Hspec (xit) import Test.Hspec.WebDriver hiding (runWD, click, uploadFile, WD) import qualified Test.Hspec.WebDriver as WD import Test.WebDriver (WD(..)) import qualified Data.ByteString.Lazy as LBS import qualified Data.Dependent.Map as DMap import qualified Data.IntMap as IntMap import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified GHCJS.DOM.File as File import qualified Network.Wai.Handler.Warp as Warp import qualified System.FilePath as FilePath import qualified Test.WebDriver as WD import qualified Test.WebDriver.Capabilities as WD import Test.Util.ChromeFlags import Test.Util.UnshareNetwork -- ORPHAN: https://github.com/kallisti-dev/hs-webdriver/pull/167 deriving instance MonadMask WD chromium :: FilePath chromium = $(staticWhich "chromium") seleniumPort, jsaddlePort :: PortNumber seleniumPort = 8000 jsaddlePort = 8001 -- TODO Remove orphan instance MonadRef WD where type Ref WD = Ref IO newRef = WD . newRef readRef = WD . readRef writeRef r = WD . writeRef r assertEqual :: (MonadIO m, Eq a, Show a) => String -> a -> a -> m () assertEqual msg a b = liftIO $ HUnit.assertEqual msg a b assertFailure :: MonadIO m => String -> m () assertFailure = liftIO . HUnit.assertFailure assertBool :: (MonadIO m) => String -> Bool -> m () assertBool msg bool = liftIO $ HUnit.assertBool msg bool chromeConfig :: Text -> [Text] -> WD.WDConfig chromeConfig fp flags = WD.useBrowser (WD.chrome { WD.chromeBinary = Just $ T.unpack fp, WD.chromeOptions = T.unpack <$> flags }) WD.defaultConfig keyMap :: DMap DKey Identity keyMap = DMap.fromList [ Key_Int ==> 0 , Key_Char ==> 'A' ] data DKey a where Key_Int :: DKey Int Key_Char :: DKey Char Key_Bool :: DKey Bool textKey :: DKey a -> Text textKey = \case Key_Int -> "Key_Int" Key_Char -> "Key_Char" Key_Bool -> "Key_Bool" deriveArgDict ''DKey deriveGEq ''DKey deriveGCompare ''DKey deriveGShow ''DKey deriving instance MonadFail WD main :: IO () main = do unshareNetwork isHeadless <- (== Nothing) <$> lookupEnv "NO_HEADLESS" withSandboxedChromeFlags isHeadless $ \chromeFlags -> do withSeleniumServer $ \selenium -> do let browserPath = T.strip $ T.pack chromium when (T.null browserPath) $ fail "No browser found" withDebugging <- isNothing <$> lookupEnv "NO_DEBUG" let wdConfig = WD.defaultConfig { WD.wdPort = fromIntegral $ _selenium_portNumber selenium } chromeCaps' = WD.getCaps $ chromeConfig browserPath chromeFlags hspec (tests withDebugging wdConfig [(chromeCaps', "chrome")] selenium) `finally` _selenium_stopServer selenium tests :: Bool -> WD.WDConfig -> [(Capabilities, String)] -> Selenium -> Spec tests withDebugging wdConfig caps _selenium = do let putStrLnDebug :: MonadIO m => Text -> m () putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m session' t = sessionWith wdConfig t . using caps runWD m = runWDOptions (WdOptions False) $ do putStrLnDebug "before" r <- m putStrLnDebug "after" return r testWidgetStatic :: WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b testWidgetStatic = testWidgetStaticDebug withDebugging testWidget :: WD () -> WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b testWidget = testWidgetDebug True withDebugging testWidget' :: WD a -> (a -> WD b) -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b testWidget' = testWidgetDebug' True withDebugging session' "text" $ do it "works" $ runWD $ do testWidgetStatic (checkBodyText "hello world") $ do text "hello world" it "works with postBuild" $ runWD $ do testWidgetStatic (checkBodyText "pb") $ do pb <- getPostBuild void $ textNode $ TextNodeConfig "" $ Just $ "pb" <$ pb it "works for adjacent text nodes" $ runWD $ do testWidgetStatic (checkBodyText "hello world") $ do text "hello " text "world" it "works for empty adjacent text nodes" $ runWD $ do testWidgetStatic (checkBodyText "hello world") $ do pb <- getPostBuild text "" text "" _ <- textNode $ TextNodeConfig "" $ Just $ "hello " <$ pb _ <- textNode $ TextNodeConfig "abc" $ Just $ "" <$ pb _ <- textNode $ TextNodeConfig "" $ Just $ "world" <$ pb text "" it "works when empty text nodes are the only children of an element" $ runWD $ do testWidgetStatic (checkBodyText "hello world") $ do el "div" $ do text "" text "" text "hello world" it "works when an empty text node is the first child before text" $ runWD $ do testWidgetStatic (checkTextInTag "div" "hello world") $ do el "div" $ do text "" text "hello world" it "works when an empty text node is the first child before element" $ runWD $ do testWidgetStatic (checkTextInTag "div" "hello world") $ do el "div" $ do text "" el "span" $ text "hello world" it "works when an empty text node is the last child" $ runWD $ do testWidgetStatic (checkTextInTag "div" "hello world") $ do el "div" $ do el "span" $ text "hello world" text "" -- TODO I think these two tests are broken -- it "updates after postBuild" $ runWD $ do -- testWidget (checkBodyText "initial") (checkBodyText "after") $ do -- after <- delay 0 =<< getPostBuild -- void $ textNode $ TextNodeConfig "initial" $ Just $ "after" <$ after -- it "updates immediately after postBuild" $ runWD $ do -- testWidget (checkBodyText "pb") (checkBodyText "after") $ do -- pb <- getPostBuild -- after <- delay 0 pb -- void $ textNode $ TextNodeConfig "initial" $ Just $ leftmost ["pb" <$ pb, "after" <$ after] it "updates in immediate mode" $ runWD $ do let checkUpdated = do checkBodyText "initial" WD.click =<< findElemWithRetry (WD.ByTag "button") checkBodyText "after" testWidget (pure ()) checkUpdated $ prerender_ (pure ()) $ do click <- button "" void $ textNode $ TextNodeConfig "initial" $ Just $ "after" <$ click session' "element" $ do it "works with domEvent Click" $ runWD $ do clickedRef <- liftIO $ newRef False testWidget' (findElemWithRetry $ WD.ByTag "div") WD.click $ do (e, _) <- el' "div" $ text "hello world" performEvent_ $ liftIO (writeRef clickedRef True) <$ domEvent Click e readRef clickedRef `shouldBeWithRetryM` True it "works with eventFlags stopPropagation" $ runWD $ do firstClickedRef <- newRef False secondClickedRef <- newRef False let clickBoth = do findElemWithRetry (WD.ById "first") >>= WD.click findElemWithRetry (WD.ById "second") >>= WD.click testWidget (pure ()) clickBoth $ do (firstDivEl, _) <- el' "div" $ prerender_ (pure ()) $ do void $ elAttr "span" ("id" =: "first") $ text "hello world" performEvent_ $ liftIO (writeRef firstClickedRef True) <$ domEvent Click firstDivEl (secondDivEl, _) <- el' "div" $ prerender_ (pure ()) $ do let conf :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace conf = (def :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace) & initialAttributes .~ "id" =: "second" & elementConfig_eventSpec .~ addEventSpecFlags (Proxy :: Proxy GhcjsDomSpace) Click (\_ -> stopPropagation) def void $ element "span" conf $ text "hello world" performEvent_ $ liftIO (writeRef secondClickedRef True) <$ domEvent Click secondDivEl firstClicked <- readRef firstClickedRef secondClicked <- readRef secondClickedRef assertEqual "Click propagated when it should have stopped" (True, False) (firstClicked, secondClicked) it "works with eventFlags preventDefault" $ runWD $ do let click = do e <- findElemWithRetry $ WD.ByTag "input" s0 <- WD.isSelected e WD.click e s1 <- WD.isSelected e pure (s0, s1) clicked <- testWidget (pure ()) click $ prerender_ (pure ()) $ do let conf :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace conf = (def :: ElementConfig EventResult (SpiderTimeline Global) GhcjsDomSpace) & elementConfig_eventSpec .~ addEventSpecFlags (Proxy :: Proxy GhcjsDomSpace) Click (\_ -> preventDefault) def & initialAttributes .~ "type" =: "checkbox" void $ element "input" conf $ text "hello world" assertEqual "Click not prevented" (False, False) clicked it "can add/update/remove attributes" $ runWD $ do let checkInitialAttrs = do e <- findElemWithRetry $ WD.ByTag "div" assertAttr e "const" (Just "const") assertAttr e "delete" (Just "delete") assertAttr e "init" (Just "init") assertAttr e "click" Nothing pure e checkModifyAttrs e = do WD.click e withRetry $ do assertAttr e "const" (Just "const") assertAttr e "delete" Nothing assertAttr e "init" (Just "click") assertAttr e "click" (Just "click") testWidget' checkInitialAttrs checkModifyAttrs $ mdo let conf = def & initialAttributes .~ "const" =: "const" <> "delete" =: "delete" <> "init" =: "init" & modifyAttributes .~ (("delete" =: Nothing <> "init" =: Just "click" <> "click" =: Just "click") <$ click) (e, ()) <- element "div" conf $ text "hello world" let click = domEvent Click e return () session' "inputElement" $ do describe "static renderer" $ do it "sets value attribute" $ runWD $ do let checkStatic = do e <- findElemWithRetry $ WD.ByTag "input" WD.attr e "value" `shouldBeWithRetryM` Just "test" pure e checkHydrated e = do WD.attr e "value" `shouldBeWithRetryM` Just "test" testWidget' checkStatic checkHydrated $ void $ inputElement $ def & inputElementConfig_initialValue .~ "test" it "updates value attribute at postBuild" $ runWD $ do let checkStatic = do e <- findElemWithRetry $ WD.ByTag "input" WD.attr e "value" `shouldBeWithRetryM` Just "test-updated" pure e checkHydrated e = do WD.attr e "value" `shouldBeWithRetryM` Just "test-updated" testWidget' checkStatic checkHydrated $ do pb <- getPostBuild _ <- inputElement $ def & inputElementConfig_initialValue .~ "test" & inputElementConfig_setValue .~ ("test-updated" <$ pb) pure () it "sets checked attr appropriately" $ runWD $ do setCheckedChan <- liftIO newChan let checkStatic = do e <- findElemWithRetry $ WD.ByTag "input" WD.attr e "checked" `shouldBeWithRetryM` Just "true" pure e checkValue e = do WD.attr e "checked" `shouldBeWithRetryM` Just "true" WD.moveToCenter e WD.click e -- Click to uncheck WD.attr e "checked" `shouldBeWithRetryM` Nothing liftIO $ writeChan setCheckedChan True -- Programatically check the checkbox WD.attr e "checked" `shouldBeWithRetryM` Just "true" testWidget' checkStatic checkValue $ do setChecked <- triggerEventWithChan setCheckedChan _ <- inputElement $ def & initialAttributes .~ "type" =: "checkbox" & inputElementConfig_initialChecked .~ True & inputElementConfig_setChecked .~ setChecked pure () it "sets checked attr appropriately at postbuild" $ runWD $ do let checkStatic = do e <- findElemWithRetry $ WD.ByTag "input" WD.attr e "checked" `shouldBeWithRetryM` Just "true" pure e checkValue e = do WD.attr e "checked" `shouldBeWithRetryM` Just "true" testWidget' checkStatic checkValue $ do pb <- getPostBuild _ <- inputElement $ def & initialAttributes .~ "type" =: "checkbox" & inputElementConfig_initialChecked .~ False & inputElementConfig_setChecked .~ (True <$ pb) pure () describe "hydration" $ do it "doesn't wipe user input when switching over" $ runWD $ do inputRef <- newRef ("hello " :: Text) testWidget' (do e <- findElemWithRetry $ WD.ByTag "input" WD.sendKeys "world" e pure e) (\e -> do WD.attr e "value" `shouldBeWithRetryM` Just "hello world" WD.click =<< findElemWithRetry (WD.ByTag "button") readRef inputRef `shouldBeWithRetryM` "hello world" ) $ do e <- inputElement $ def & inputElementConfig_initialValue .~ "hello " click <- button "save" performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click it "captures user input after switchover" $ runWD $ do inputRef <- newRef ("" :: Text) let checkValue = do WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "input") WD.click =<< findElemWithRetry (WD.ByTag "button") readRef inputRef `shouldBeWithRetryM` "hello world" testWidget (pure ()) checkValue $ do e <- inputElement def click <- button "save" performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click it "sets focus appropriately" $ runWD $ do focusRef <- newRef False let checkValue = do readRef focusRef `shouldBeWithRetryM` False e <- findElemWithRetry $ WD.ByTag "input" WD.click e readRef focusRef `shouldBeWithRetryM` True testWidget (pure ()) checkValue $ do e <- inputElement def performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e) it "sets focus when focus occurs before hydration" $ runWD $ do focusRef <- newRef False let setup = do e <- findElemWithRetry $ WD.ByTag "input" WD.click e ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True readRef focusRef `shouldBeWithRetryM` False check = readRef focusRef `shouldBeWithRetryM` True testWidget setup check $ do e <- inputElement def performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e) it "sets value appropriately" $ runWD $ do valueByUIRef <- newRef ("" :: Text) valueRef <- newRef ("" :: Text) setValueChan :: Chan Text <- liftIO newChan let checkValue = do readRef valueByUIRef `shouldBeWithRetryM` "" readRef valueRef `shouldBeWithRetryM` "" e <- findElemWithRetry $ WD.ByTag "input" WD.sendKeys "hello" e readRef valueByUIRef `shouldBeWithRetryM` "hello" readRef valueRef `shouldBeWithRetryM` "hello" liftIO $ writeChan setValueChan "world" readRef valueByUIRef `shouldBeWithRetryM` "hello" readRef valueRef `shouldBeWithRetryM` "world" testWidget (pure ()) checkValue $ do update <- triggerEventWithChan setValueChan e <- inputElement $ def & inputElementConfig_setValue .~ update performEvent_ $ liftIO . writeRef valueByUIRef <$> _inputElement_input e performEvent_ $ liftIO . writeRef valueRef <$> updated (value e) it "sets checked appropriately" $ runWD $ do checkedByUIRef <- newRef False checkedRef <- newRef False setCheckedChan <- liftIO newChan let checkStatic = do e <- findElemWithRetry $ WD.ByTag "input" WD.attr e "checked" `shouldBeWithRetryM` Nothing pure e checkValue e = do readRef checkedByUIRef `shouldBeWithRetryM` False readRef checkedRef `shouldBeWithRetryM` False WD.attr e "checked" `shouldBeWithRetryM` Nothing WD.moveToCenter e WD.click e readRef checkedByUIRef `shouldBeWithRetryM` True readRef checkedRef `shouldBeWithRetryM` True WD.attr e "checked" `shouldBeWithRetryM` Just "true" liftIO $ writeChan setCheckedChan False readRef checkedByUIRef `shouldBeWithRetryM` True readRef checkedRef `shouldBeWithRetryM` False WD.attr e "checked" `shouldBeWithRetryM` Nothing testWidget' checkStatic checkValue $ do setChecked <- triggerEventWithChan setCheckedChan e <- inputElement $ def & initialAttributes .~ "type" =: "checkbox" & inputElementConfig_setChecked .~ setChecked performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e) it "respects user updates to checked which happen before hydration" $ runWD $ do checkedByUIRef <- newRef False checkedRef <- newRef False let checkStatic = do e <- findElemWithRetry $ WD.ByTag "input" WD.attr e "checked" `shouldBeWithRetryM` Nothing WD.moveToCenter e WD.click e WD.attr e "checked" `shouldBeWithRetryM` Just "true" pure e checkValue e = do WD.attr e "checked" `shouldBeWithRetryM` Just "true" readRef checkedByUIRef `shouldBeWithRetryM` True readRef checkedRef `shouldBeWithRetryM` True testWidget' checkStatic checkValue $ do e <- inputElement $ def & initialAttributes .~ "type" =: "checkbox" performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e) it "captures file uploads" $ runWD $ do filesRef :: IORef [Text] <- newRef [] let uploadFile = do e <- findElemWithRetry $ WD.ByTag "input" path <- liftIO $ writeSystemTempFile "testFile" "file contents" WD.sendKeys (T.pack path) e WD.click =<< findElemWithRetry (WD.ByTag "button") liftIO $ removeFile path readRef filesRef `shouldBeWithRetryM` [T.pack $ FilePath.takeFileName path] testWidget (pure ()) uploadFile $ do e <- inputElement $ def & initialAttributes .~ "type" =: "file" click <- button "save" prerender_ (pure ()) $ performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do names <- liftJSM $ traverse File.getName fs liftIO $ writeRef filesRef names it "fires _input event if the user altered the value before hydration" $ runWD $ do input <- newRef ("" :: Text) update <- newRef ("" :: Text) let checkStatic = do e <- findElemWithRetry $ WD.ByTag "input" WD.attr e "value" `shouldBeWithRetryM` Just "" WD.sendKeys "test" e WD.attr e "value" `shouldBeWithRetryM` Just "test" pure e checkHydrated e = do WD.attr e "value" `shouldBeWithRetryM` Just "test" readRef input `shouldBeWithRetryM` "test" readRef update `shouldBeWithRetryM` "test" testWidget' checkStatic checkHydrated $ do e <- inputElement def performEvent_ $ liftIO . writeRef input <$> _inputElement_input e performEvent_ $ liftIO . writeRef update <$> updated (_inputElement_value e) it "does not fire _input event when the value is updated at postBuild" $ runWD $ do input <- newRef (Nothing :: Maybe Text) let checkStatic = do e <- findElemWithRetry $ WD.ByTag "input" WD.attr e "value" `shouldBeWithRetryM` Just "pb" pure e checkHydrated e = do WD.attr e "value" `shouldBeWithRetryM` Just "pb" readRef input `shouldBeWithRetryM` Nothing testWidget' checkStatic checkHydrated $ do pb <- getPostBuild e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb) performEvent_ $ liftIO . writeRef input . Just <$> _inputElement_input e it "SSR produces correct DOM based on inputElement values when setValue happens at postBuild" $ runWD $ do let checkBoth = do input <- findElemWithRetry $ WD.ByTag "input" WD.attr input "value" `shouldBeWithRetryM` Just "pb" p <- findElemWithRetry (WD.ByTag "p") shouldContainText "pb" p testWidget checkBoth checkBoth $ do pb <- getPostBuild e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb) el "p" $ dynText $ _inputElement_value e it "does not fail when both setValue AND user updated value happen before switchover" $ runWD $ do let checkStatic = do input <- findElemWithRetry $ WD.ByTag "input" h2_value <- findElemWithRetry (WD.ByTag "h2") h3_input <- findElemWithRetry (WD.ByTag "h3") WD.attr input "value" `shouldBeWithRetryM` Just "pb" shouldContainText "pb" h2_value shouldContainText "" h3_input WD.sendKeys "abc" input WD.attr input "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pb" h2_value shouldContainText "" h3_input pure (input, h2_value, h3_input) checkHydrated (input, h2_value, h3_input) = do WD.attr input "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pbabc" h3_input shouldContainText "pbabc pb" h2_value pure () testWidget' checkStatic checkHydrated $ do pb <- getPostBuild e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb) el "h1" $ dynText $ _inputElement_value e el "h2" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ updated $ _inputElement_value e el "h3" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ _inputElement_input e it "value is correct when both setValue AND user updated value happen before switchover" $ runWD $ do let checkStatic = do input <- findElemWithRetry $ WD.ByTag "input" p <- findElemWithRetry (WD.ByTag "p") WD.attr input "value" `shouldBeWithRetryM` Just "pb" shouldContainText "pb" p WD.sendKeys "abc" input WD.attr input "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pb" p -- It won't be updated yet pure (input, p) checkHydrated (input, p) = do WD.attr input "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pbabc" p pure () testWidget' checkStatic checkHydrated $ do pb <- getPostBuild e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb) el "p" $ dynText $ _inputElement_value e it "input event and (updated value) fire correctly when both setValue AND user updated value happen before switchover" $ runWD $ do valRef <- newRef ([] :: [Text]) inputRef <- newRef ([] :: [Text]) let consRef ref a = liftIO $ atomicModifyRef ref $ \as -> (a:as, ()) checkStatic = do input <- findElemWithRetry $ WD.ByTag "input" p <- findElemWithRetry (WD.ByTag "p") WD.attr input "value" `shouldBeWithRetryM` Just "pb" shouldContainText "pb" p WD.sendKeys "abc" input WD.attr input "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pb" p -- It won't be updated yet readRef inputRef `shouldBeWithRetryM` [] -- Should never fire input ref during SSR readRef valRef `shouldBeWithRetryM` ["pb"] pure (input, p) checkHydrated (input, p) = do readRef inputRef `shouldBeWithRetryM` ["pbabc"] readRef valRef `shouldBeWithRetryM` ["pbabc", "pb"] WD.attr input "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pbabc" p pure () testWidget' checkStatic checkHydrated $ do liftIO $ writeRef valRef [] pb <- getPostBuild e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb) el "p" $ dynText $ _inputElement_value e performEvent_ $ consRef valRef <$> updated (_inputElement_value e) performEvent_ $ consRef inputRef <$> _inputElement_input e describe "hydration/immediate" $ do it "captures user input after switchover" $ runWD $ do inputRef :: IORef Text <- newRef "" let checkValue = do WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "input") WD.click =<< findElemWithRetry (WD.ByTag "button") readRef inputRef `shouldBeWithRetryM` "hello world" testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do e <- inputElement def click <- button "save" performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click it "sets focus appropriately" $ runWD $ do focusRef <- newRef False let checkValue = do readRef focusRef `shouldBeWithRetryM` False e <- findElemWithRetry $ WD.ByTag "input" WD.click e readRef focusRef `shouldBeWithRetryM` True testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do e <- inputElement def performEvent_ $ liftIO . writeRef focusRef <$> updated (_inputElement_hasFocus e) it "sets value appropriately" $ runWD $ do valueByUIRef :: IORef Text <- newRef "" valueRef :: IORef Text <- newRef "" setValueChan :: Chan Text <- liftIO newChan let checkValue = do readRef valueByUIRef `shouldBeWithRetryM` "" readRef valueRef `shouldBeWithRetryM` "" e <- findElemWithRetry $ WD.ByTag "input" WD.sendKeys "hello" e readRef valueByUIRef `shouldBeWithRetryM` "hello" readRef valueRef `shouldBeWithRetryM` "hello" liftIO $ writeChan setValueChan "world" readRef valueByUIRef `shouldBeWithRetryM` "hello" readRef valueRef `shouldBeWithRetryM` "world" testWidget (pure ()) checkValue $ do update <- triggerEventWithChan setValueChan prerender_ (pure ()) $ do e <- inputElement $ def & inputElementConfig_setValue .~ update performEvent_ $ liftIO . writeRef valueByUIRef <$> _inputElement_input e performEvent_ $ liftIO . writeRef valueRef <$> updated (value e) it "sets checked appropriately" $ runWD $ do checkedByUIRef <- newRef False checkedRef <- newRef False setCheckedChan <- liftIO newChan let checkValue = do readRef checkedByUIRef `shouldBeWithRetryM` False readRef checkedRef `shouldBeWithRetryM` False e <- findElemWithRetry $ WD.ByTag "input" WD.moveToCenter e WD.click e readRef checkedByUIRef `shouldBeWithRetryM` True readRef checkedRef `shouldBeWithRetryM` True liftIO $ writeChan setCheckedChan False readRef checkedByUIRef `shouldBeWithRetryM` True readRef checkedRef `shouldBeWithRetryM` False testWidget (pure ()) checkValue $ do setChecked <- triggerEventWithChan setCheckedChan prerender_ (pure ()) $ do e <- inputElement $ def & initialAttributes .~ "type" =: "checkbox" & inputElementConfig_setChecked .~ setChecked performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e) it "captures file uploads" $ runWD $ do filesRef :: IORef [Text] <- newRef [] let uploadFile = do e <- findElemWithRetry $ WD.ByTag "input" path <- liftIO $ writeSystemTempFile "testFile" "file contents" WD.sendKeys (T.pack path) e WD.click =<< findElemWithRetry (WD.ByTag "button") liftIO $ removeFile path readRef filesRef `shouldBeWithRetryM` [T.pack $ FilePath.takeFileName path] testWidget (pure ()) uploadFile $ prerender_ (pure ()) $ do e <- inputElement $ def & initialAttributes .~ "type" =: "file" click <- button "save" performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do names <- liftJSM $ traverse File.getName fs liftIO $ writeRef filesRef names session' "textAreaElement" $ do describe "static renderer" $ do it "sets value attribute" $ runWD $ do let checkStatic = do e <- findElemWithRetry $ WD.ByTag "textarea" WD.attr e "value" `shouldBeWithRetryM` Just "test" pure e checkHydrated e = do WD.attr e "value" `shouldBeWithRetryM` Just "test" testWidget' checkStatic checkHydrated $ void $ textAreaElement $ def & textAreaElementConfig_initialValue .~ "test" it "updates value attribute at postBuild" $ runWD $ do let checkStatic = do e <- findElemWithRetry $ WD.ByTag "textarea" WD.attr e "value" `shouldBeWithRetryM` Just "test-updated" pure e checkHydrated e = do WD.attr e "value" `shouldBeWithRetryM` Just "test-updated" testWidget' checkStatic checkHydrated $ do pb <- getPostBuild _ <- textAreaElement $ def & textAreaElementConfig_initialValue .~ "test" & textAreaElementConfig_setValue .~ ("test-updated" <$ pb) pure () describe "hydration" $ do it "doesn't wipe user input when switching over" $ runWD $ do inputRef <- newRef ("" :: Text) testWidget' (do e <- findElemWithRetry $ WD.ByTag "textarea" WD.sendKeys "hello world" e pure e) (\e -> do WD.attr e "value" `shouldBeWithRetryM` Just "hello world" WD.click <=< findElemWithRetry $ WD.ByTag "button" readRef inputRef `shouldBeWithRetryM` "hello world" ) $ do e <- textAreaElement def click <- button "save" performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click it "captures user input after switchover" $ runWD $ do inputRef <- newRef ("" :: Text) let checkValue = do WD.sendKeys "hello world" =<< findElemWithRetry (WD.ByTag "textarea") WD.click =<< findElemWithRetry (WD.ByTag "button") readRef inputRef `shouldBeWithRetryM` "hello world" testWidget (pure ()) checkValue $ do e <- textAreaElement def click <- button "save" performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click it "sets focus appropriately" $ runWD $ do focusRef <- newRef False let checkValue = do readRef focusRef `shouldBeWithRetryM` False e <- findElemWithRetry $ WD.ByTag "textarea" WD.click e readRef focusRef `shouldBeWithRetryM` True testWidget (pure ()) checkValue $ do e <- textAreaElement def performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e) it "sets focus when focus occurs before hydration" $ runWD $ do focusRef <- newRef False let setup = do e <- findElemWithRetry $ WD.ByTag "textarea" WD.click e ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True readRef focusRef `shouldBeWithRetryM` False check = readRef focusRef `shouldBeWithRetryM` True testWidget setup check $ do e <- textAreaElement def performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e) it "sets value appropriately" $ runWD $ do valueByUIRef <- newRef ("" :: Text) valueRef <- newRef ("" :: Text) setValueChan :: Chan Text <- liftIO newChan let checkValue = do readRef valueByUIRef `shouldBeWithRetryM` "" readRef valueRef `shouldBeWithRetryM` "" e <- findElemWithRetry $ WD.ByTag "textarea" WD.sendKeys "hello" e readRef valueByUIRef `shouldBeWithRetryM` "hello" readRef valueRef `shouldBeWithRetryM` "hello" liftIO $ writeChan setValueChan "world" readRef valueByUIRef `shouldBeWithRetryM` "hello" readRef valueRef `shouldBeWithRetryM` "world" testWidget (pure ()) checkValue $ do setValue' <- triggerEventWithChan setValueChan e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' } performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e performEvent_ $ liftIO . writeRef valueRef <$> updated (value e) it "fires _input event if the user altered the value before hydration" $ runWD $ do textarea <- newRef ("" :: Text) update <- newRef ("" :: Text) let checkStatic = do e <- findElemWithRetry $ WD.ByTag "textarea" WD.attr e "value" `shouldBeWithRetryM` Just "" WD.sendKeys "test" e WD.attr e "value" `shouldBeWithRetryM` Just "test" pure e checkHydrated e = do WD.attr e "value" `shouldBeWithRetryM` Just "test" readRef textarea `shouldBeWithRetryM` "test" readRef update `shouldBeWithRetryM` "test" testWidget' checkStatic checkHydrated $ do e <- textAreaElement def performEvent_ $ liftIO . writeRef textarea <$> _textAreaElement_input e performEvent_ $ liftIO . writeRef update <$> updated (_textAreaElement_value e) it "does not fire _input event when the value is updated at postBuild" $ runWD $ do textarea <- newRef (Nothing :: Maybe Text) let checkStatic = do e <- findElemWithRetry $ WD.ByTag "textarea" WD.attr e "value" `shouldBeWithRetryM` Just "pb" pure e checkHydrated e = do WD.attr e "value" `shouldBeWithRetryM` Just "pb" readRef textarea `shouldBeWithRetryM` Nothing testWidget' checkStatic checkHydrated $ do pb <- getPostBuild e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb) performEvent_ $ liftIO . writeRef textarea . Just <$> _textAreaElement_input e it "SSR produces correct DOM based on textAreaElement values when setValue happens at postBuild" $ runWD $ do let checkBoth = do textarea <- findElemWithRetry $ WD.ByTag "textarea" WD.attr textarea "value" `shouldBeWithRetryM` Just "pb" p <- findElemWithRetry (WD.ByTag "p") shouldContainText "pb" p testWidget checkBoth checkBoth $ do pb <- getPostBuild e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb) el "p" $ dynText $ _textAreaElement_value e it "does not fail when both setValue AND user updated value happen before switchover" $ runWD $ do let checkStatic = do textarea <- findElemWithRetry $ WD.ByTag "textarea" h2_value <- findElemWithRetry (WD.ByTag "h2") h3_input <- findElemWithRetry (WD.ByTag "h3") WD.attr textarea "value" `shouldBeWithRetryM` Just "pb" shouldContainText "pb" h2_value shouldContainText "" h3_input WD.sendKeys "abc" textarea WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pb" h2_value shouldContainText "" h3_input pure (textarea, h2_value, h3_input) checkHydrated (textarea, h2_value, h3_input) = do WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pbabc" h3_input shouldContainText "pbabc pb" h2_value pure () testWidget' checkStatic checkHydrated $ do pb <- getPostBuild e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb) el "h1" $ dynText $ _textAreaElement_value e el "h2" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ updated $ _textAreaElement_value e el "h3" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ _textAreaElement_input e it "value is correct when both setValue AND user updated value happen before switchover" $ runWD $ do let checkStatic = do textarea <- findElemWithRetry $ WD.ByTag "textarea" p <- findElemWithRetry (WD.ByTag "p") WD.attr textarea "value" `shouldBeWithRetryM` Just "pb" shouldContainText "pb" p WD.sendKeys "abc" textarea WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pb" p -- It won't be updated yet pure (textarea, p) checkHydrated (textarea, p) = do WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc" shouldContainText "pbabc" p pure () testWidget' checkStatic checkHydrated $ do pb <- getPostBuild e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb) el "p" $ dynText $ _textAreaElement_value e describe "hydration/immediate" $ do it "captures user input after switchover" $ runWD $ do inputRef :: IORef Text <- newRef "" let checkValue = do WD.sendKeys "hello world" <=< findElemWithRetry $ WD.ByTag "textarea" WD.click <=< findElemWithRetry $ WD.ByTag "button" readRef inputRef `shouldBeWithRetryM` "hello world" testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do e <- textAreaElement def click <- button "save" performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click it "sets focus appropriately" $ runWD $ do focusRef <- newRef False let checkValue = do readRef focusRef `shouldBeWithRetryM` False e <- findElemWithRetry $ WD.ByTag "textarea" WD.click e readRef focusRef `shouldBeWithRetryM` True testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do e <- textAreaElement def performEvent_ $ liftIO . writeRef focusRef <$> updated (_textAreaElement_hasFocus e) it "sets value appropriately" $ runWD $ do valueByUIRef :: IORef Text <- newRef "" valueRef :: IORef Text <- newRef "" setValueChan :: Chan Text <- liftIO newChan let checkValue = do readRef valueByUIRef `shouldBeWithRetryM` "" readRef valueRef `shouldBeWithRetryM` "" e <- findElemWithRetry $ WD.ByTag "textarea" WD.sendKeys "hello" e readRef valueByUIRef `shouldBeWithRetryM` "hello" readRef valueRef `shouldBeWithRetryM` "hello" liftIO $ writeChan setValueChan "world" readRef valueByUIRef `shouldBeWithRetryM` "hello" readRef valueRef `shouldBeWithRetryM` "world" testWidget (pure ()) checkValue $ do setValue' <- triggerEventWithChan setValueChan prerender_ (pure ()) $ do e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' } performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e performEvent_ $ liftIO . writeRef valueRef <$> updated (value e) session' "selectElement" $ do let options :: DomBuilder t m => m () options = do elAttr "option" ("value" =: "one" <> "id" =: "one") $ text "one" elAttr "option" ("value" =: "two" <> "id" =: "two") $ text "two" elAttr "option" ("value" =: "three" <> "id" =: "three") $ text "three" describe "hydration" $ do it "sets initial value correctly" $ runWD $ do inputRef <- newRef ("" :: Text) let setup = do e <- findElemWithRetry $ WD.ByTag "select" assertAttr e "value" (Just "three") WD.click =<< findElemWithRetry (WD.ById "two") pure e check e = do assertAttr e "value" (Just "two") readRef inputRef `shouldBeWithRetryM` "three" WD.click =<< findElemWithRetry (WD.ByTag "button") assertAttr e "value" (Just "two") readRef inputRef `shouldBeWithRetryM` "two" testWidget' setup check $ do (e, ()) <- selectElement (def { _selectElementConfig_initialValue = "three" }) options click <- button "save" liftIO . writeRef inputRef <=< sample $ current $ _selectElement_value e performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click it "captures user input after switchover" $ runWD $ do inputRef <- newRef ("" :: Text) let checkValue = do e <- findElemWithRetry $ WD.ByTag "select" assertAttr e "value" (Just "one") WD.click =<< findElemWithRetry (WD.ById "two") assertAttr e "value" (Just "two") WD.click =<< findElemWithRetry (WD.ByTag "button") readRef inputRef `shouldBeWithRetryM` "two" testWidget (pure ()) checkValue $ do (e, ()) <- selectElement def options click <- button "save" performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click it "sets focus appropriately" $ runWD $ do focusRef <- newRef False let checkValue = do readRef focusRef `shouldBeWithRetryM` False e <- findElemWithRetry $ WD.ByTag "select" WD.click e readRef focusRef `shouldBeWithRetryM` True testWidget (pure ()) checkValue $ do (e, ()) <- selectElement def options performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e) it "sets focus when focus occurs before hydration" $ runWD $ do focusRef <- newRef False let setup = do e <- findElemWithRetry $ WD.ByTag "select" WD.click e ((== e) <$> WD.activeElem) `shouldBeWithRetryM` True readRef focusRef `shouldBeWithRetryM` False check = readRef focusRef `shouldBeWithRetryM` True testWidget setup check $ do (e, ()) <- selectElement def options performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e) it "sets value appropriately" $ runWD $ do valueByUIRef <- newRef ("" :: Text) valueRef <- newRef ("" :: Text) setValueChan :: Chan Text <- liftIO newChan let checkValue = do e <- findElemWithRetry $ WD.ByTag "select" assertAttr e "value" (Just "one") readRef valueByUIRef `shouldBeWithRetryM` "one" readRef valueRef `shouldBeWithRetryM` "one" WD.click <=< findElemWithRetry $ WD.ById "two" readRef valueByUIRef `shouldBeWithRetryM` "two" readRef valueRef `shouldBeWithRetryM` "two" liftIO $ writeChan setValueChan "three" readRef valueByUIRef `shouldBeWithRetryM` "two" readRef valueRef `shouldBeWithRetryM` "three" testWidget (pure ()) checkValue $ do setValue' <- triggerEventWithChan setValueChan (e, ()) <- selectElement def { _selectElementConfig_setValue = Just setValue' } options performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e) describe "hydration/immediate" $ do it "captures user input after switchover" $ runWD $ do inputRef :: IORef Text <- newRef "" let checkValue = do WD.click <=< findElemWithRetry $ WD.ById "two" WD.click <=< findElemWithRetry $ WD.ByTag "button" readRef inputRef `shouldBeWithRetryM` "two" testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do (e, ()) <- selectElement def options click <- button "save" performEvent_ $ liftIO . writeRef inputRef <$> tag (current (_selectElement_value e)) click it "sets focus appropriately" $ runWD $ do focusRef <- newRef False let checkValue = do readRef focusRef `shouldBeWithRetryM` False e <- findElemWithRetry $ WD.ByTag "select" WD.click e readRef focusRef `shouldBeWithRetryM` True testWidget (pure ()) checkValue $ prerender_ (pure ()) $ do (e, ()) <- selectElement def options performEvent_ $ liftIO . writeRef focusRef <$> updated (_selectElement_hasFocus e) it "has correct initial value" $ runWD $ do valueRef :: IORef Text <- newRef "" let checkValue = readRef valueRef `shouldBeWithRetryM` "one" testWidget (pure ()) checkValue $ do prerender_ (pure ()) $ do (e, ()) <- selectElement def { _selectElementConfig_initialValue = "one" } options liftIO . writeRef valueRef <=< sample $ current $ _selectElement_value e it "sets value appropriately" $ runWD $ do valueByUIRef :: IORef Text <- newRef "" valueRef :: IORef Text <- newRef "" setValueChan :: Chan Text <- liftIO newChan let checkValue = do WD.click =<< findElemWithRetry (WD.ById "two") readRef valueByUIRef `shouldBeWithRetryM` "two" readRef valueRef `shouldBeWithRetryM` "two" liftIO $ writeChan setValueChan "three" readRef valueByUIRef `shouldBeWithRetryM` "two" readRef valueRef `shouldBeWithRetryM` "three" testWidget (pure ()) checkValue $ do setValue' <- triggerEventWithChan setValueChan prerender_ (pure ()) $ do (e, ()) <- selectElement def { _selectElementConfig_setValue = Just setValue' } options performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e) session' "prerender" $ do it "works in simple case" $ runWD $ do testWidget (checkBodyText "One") (checkBodyText "Two") $ do prerender_ (text "One") (text "Two") it "removes element correctly" $ runWD $ do testWidget' (findElemWithRetry $ WD.ByTag "span") elementShouldBeRemoved $ do prerender_ (el "span" $ text "One") (text "Two") it "can be nested in server widget" $ runWD $ do testWidget (checkBodyText "One") (checkBodyText "Three") $ do prerender_ (prerender_ (text "One") (text "Two")) (text "Three") it "can be nested in client widget" $ runWD $ do testWidget (checkBodyText "One") (checkBodyText "Three") $ do prerender_ (text "One") (prerender_ (text "Two") (text "Three")) it "works with prerender_ siblings" $ runWD $ do testWidget (checkTextInId "a1" "One" >> checkTextInId "b1" "Three" >> checkTextInId "c1" "Five") (checkTextInId "a2" "Two" >> checkTextInId "b2" "Four" >> checkTextInId "c2" "Six") $ do prerender_ (divId "a1" $ text "One") (divId "a2" $ text "Two") prerender_ (divId "b1" $ text "Three") (divId "b2" $ text "Four") prerender_ (divId "c1" $ text "Five") (divId "c2" $ text "Six") it "works inside other element" $ runWD $ do testWidget (checkTextInTag "div" "One") (checkTextInTag "div" "Two") $ do el "div" $ prerender_ (text "One") (text "Two") -- TODO re-enable this -- it "places fences and removes them" $ runWD $ do -- testWidget' -- (do -- scripts <- WD.findElems $ WD.ByTag "script" -- filterM (\s -> maybe False (\t -> "prerender" `T.isPrefixOf` t) <$> WD.attr s "type") scripts -- ) -- (traverse_ elementShouldBeRemoved) -- (el "span" $ prerender_ (text "One") (text "Two")) it "postBuild works on server side" $ runWD $ do lock :: MVar () <- liftIO newEmptyMVar testWidget (liftIO $ takeMVar lock) (pure ()) $ do prerender_ (do pb <- getPostBuild performEvent_ $ liftIO (putMVar lock ()) <$ pb) blank it "postBuild works on client side" $ runWD $ do lock :: MVar () <- liftIO newEmptyMVar testWidget (pure ()) (liftIO $ takeMVar lock) $ do prerender_ blank $ do pb <- getPostBuild performEvent_ $ liftIO (putMVar lock ()) <$ pb it "result Dynamic is updated *after* switchover" $ runWD $ do let preSwitchover = checkBodyText "PostBuild" check = checkBodyText "Client" testWidget preSwitchover check $ void $ do d <- prerender (pure "Initial") (pure "Client") pb <- getPostBuild initial <- sample $ current d textNode $ TextNodeConfig initial $ Just $ leftmost [updated d, "PostBuild" <$ pb] -- This essentially checks that the client IO runs *after* switchover/postBuild, -- thus can't create conflicting DOM it "can't exploit IO to break hydration" $ runWD $ do let preSwitchover = checkBodyText "Initial" testWidgetStatic preSwitchover $ void $ do ref <- liftIO $ newRef "Initial" prerender_ (pure ()) (liftIO $ writeRef ref "Client") text <=< liftIO $ readRef ref -- As above, so below it "can't exploit triggerEvent to break hydration" $ runWD $ do let preSwitchover = checkBodyText "Initial" check = checkBodyText "Client" testWidget preSwitchover check $ void $ do (e, trigger) <- newTriggerEvent prerender_ (pure ()) (liftIO $ trigger "Client") textNode $ TextNodeConfig "Initial" $ Just e session' "namespaces" $ do it "dyn can be nested in namespaced widget" $ runWD $ do testWidget (pure ()) (checkTextInTag "svg" "one") $ do let svgRootCfg = def & (elementConfig_namespace ?~ "http://www.w3.org/2000/svg") & (elementConfig_initialAttributes .~ ("width" =: "100%" <> "height" =: "100%" <> "viewBox" =: "0 0 1536 2048")) void $ element "svg" svgRootCfg $ do dyn_ $ text "one" <$ pure () session' "runWithReplace" $ do it "works" $ runWD $ do replaceChan :: Chan Text <- liftIO newChan let setup = findElemWithRetry $ WD.ByTag "div" check ssr = do -- Check that the original element still exists and has the correct text shouldContainText "two" ssr liftIO $ writeChan replaceChan "three" elementShouldBeRemoved ssr shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span") testWidget' setup check $ do replace <- triggerEventWithChan replaceChan pb <- getPostBuild void $ runWithReplace (el "div" $ text "one") $ leftmost [ el "div" (text "two") <$ pb , el "span" . text <$> replace ] it "can be nested in initial widget" $ runWD $ do replaceChan :: Chan Text <- liftIO newChan let setup = findElemWithRetry $ WD.ByTag "div" check ssr = do -- Check that the original element still exists and has the correct text shouldContainText "two" ssr liftIO $ writeChan replaceChan "three" elementShouldBeRemoved ssr shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span") testWidget' setup check $ void $ flip runWithReplace never $ do replace <- triggerEventWithChan replaceChan pb <- getPostBuild void $ runWithReplace (el "div" $ text "one") $ leftmost [ el "div" (text "two") <$ pb , el "span" . text <$> replace ] it "can be nested in postBuild widget" $ runWD $ do replaceChan :: Chan Text <- liftIO newChan let setup = findElemWithRetry $ WD.ByTag "div" check ssr = do -- Check that the original element still exists and has the correct text shouldContainText "two" ssr liftIO $ writeChan replaceChan "three" elementShouldBeRemoved ssr shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span") testWidget' setup check $ void $ do pb <- getPostBuild runWithReplace (pure ()) $ ffor pb $ \() -> do replace <- triggerEventWithChan replaceChan pb' <- getPostBuild void $ runWithReplace (el "div" $ text "one") $ leftmost [ el "div" (text "two") <$ pb' , el "span" . text <$> replace ] it "postBuild works in replaced widget" $ runWD $ do replaceChan <- liftIO newChan pbLock <- liftIO newEmptyMVar let check = liftIO $ do writeChan replaceChan () takeMVar pbLock testWidget (pure ()) check $ void $ do replace <- triggerEventWithChan replaceChan runWithReplace (pure ()) $ ffor replace $ \() -> do pb <- getPostBuild performEvent_ $ liftIO (putMVar pbLock ()) <$ pb it "can be nested in event widget" $ runWD $ do replaceChan1 :: Chan Text <- liftIO newChan replaceChan2 :: Chan Text <- liftIO newChan lock :: MVar () <- liftIO newEmptyMVar let check = do shouldContainText "" =<< findElemWithRetry (WD.ByTag "body") liftIO $ do writeChan replaceChan1 "one" takeMVar lock one <- findElemWithRetry $ WD.ByTag "div" shouldContainText "pb" one liftIO $ writeChan replaceChan2 "two" elementShouldBeRemoved one shouldContainText "two" =<< findElemWithRetry (WD.ByTag "span") testWidget (pure ()) check $ void $ do replace1 <- triggerEventWithChan replaceChan1 runWithReplace (pure ()) $ ffor replace1 $ \r1 -> do replace2 <- triggerEventWithChan replaceChan2 pb <- getPostBuild performEvent_ $ liftIO (putMVar lock ()) <$ pb void $ runWithReplace (el "div" $ text r1) $ leftmost [ el "span" . text <$> replace2 , el "div" (text "pb") <$ pb ] it "prerender works in replaced widget" $ runWD $ do replaceChan <- liftIO newChan lock :: MVar () <- liftIO newEmptyMVar pbLock :: MVar () <- liftIO newEmptyMVar let check = do liftIO $ do writeChan replaceChan () takeMVar lock takeMVar pbLock testWidget (pure ()) check $ void $ do replace <- triggerEventWithChan replaceChan runWithReplace (pure ()) $ ffor replace $ \() -> do prerender_ (pure ()) $ do liftIO $ putMVar lock () pb <- getPostBuild performEvent_ $ liftIO (putMVar pbLock ()) <$ pb it "prerender returns in immediate mode" $ runWD $ do replaceChan <- liftIO newChan pbLock <- liftIO newEmptyMVar let check = liftIO $ do writeChan replaceChan () takeMVar pbLock testWidget (pure ()) check $ void $ do replace <- triggerEventWithChan replaceChan runWithReplace (pure ()) $ ffor replace $ \() -> do prerender_ (pure ()) (pure ()) liftIO $ putMVar pbLock () it "works in immediate mode (RHS of prerender)" $ runWD $ do replaceChan :: Chan Text <- liftIO newChan let check = do checkBodyText "one" one <- findElemWithRetry $ WD.ByTag "div" shouldContainText "one" one liftIO $ writeChan replaceChan "pb" elementShouldBeRemoved one shouldContainText "pb" =<< findElemWithRetry (WD.ByTag "span") testWidget (pure ()) check $ void $ do replace <- triggerEventWithChan replaceChan prerender_ blank $ do void $ runWithReplace (el "div" $ text "one") $ el "span" . text <$> replace it "works with postBuild in immediate mode (RHS of prerender)" $ runWD $ do replaceChan :: Chan Text <- liftIO newChan let check = do checkBodyText "two" two <- findElemWithRetry $ WD.ByTag "div" shouldContainText "two" two liftIO $ writeChan replaceChan "three" elementShouldBeRemoved two shouldContainText "three" =<< findElemWithRetry (WD.ByTag "span") testWidget (pure ()) check $ void $ do replace <- triggerEventWithChan replaceChan prerender_ blank $ do pb <- getPostBuild void $ runWithReplace (el "div" $ text "one") $ leftmost [ el "div" (text "two") <$ pb , el "span" . text <$> replace ] let checkInnerHtml t x = findElemWithRetry (WD.ByTag t) >>= (`attr` "innerHTML") >>= (`shouldBe` Just x) it "removes bracketing comments" $ runWD $ do replaceChan :: Chan () <- liftIO newChan let preSwitchover = checkInnerHtml "div" "before|inner1|after" check () = do liftIO $ writeChan replaceChan () -- trigger creation of p tag _ <- findElemWithRetry $ WD.ByTag "p" -- wait till p tag is created checkInnerHtml "div" "before|

inner2

|after" testWidget' preSwitchover check $ do replace <- triggerEventWithChan replaceChan el "div" $ do text "before|" _ <- runWithReplace (text "inner1") $ el "p" (text "inner2") <$ replace text "|after" it "ignores extra ending bracketing comment" $ runWD $ do replaceChan :: Chan () <- liftIO newChan let preSwitchover = checkInnerHtml "div" "before|inner1|after" check () = do liftIO $ writeChan replaceChan () -- trigger creation of p tag _ <- findElemWithRetry $ WD.ByTag "p" -- wait till p tag is created checkInnerHtml "div" "before|inner2|after" testWidget' preSwitchover check $ do replace <- triggerEventWithChan replaceChan el "div" $ do text "before|" _ <- runWithReplace (text "inner1" *> comment "replace-end-0") $ text "inner2" <$ replace text "|after" void $ runWithReplace blank $ el "p" blank <$ replace -- Signal tag for end of test -- TODO This test actually causes a hydration failure, but it wasn't -- previously detected, so I've marked it pending xit "ignores missing ending bracketing comments" $ runWD $ do replaceChan :: Chan () <- liftIO newChan let preSwitchover = do checkInnerHtml "div" "before|inner1|after" divEl <- findElemWithRetry (WD.ByTag "div") let wrongHtml = "inner1" actualHtml :: String <- WD.executeJS [WD.JSArg divEl, WD.JSArg wrongHtml] "arguments[0].innerHTML = arguments[1]; return arguments[0].innerHTML" actualHtml `shouldBe` wrongHtml check () = do liftIO $ writeChan replaceChan () -- trigger creation of p tag _ <- findElemWithRetry $ WD.ByTag "p" -- wait till p tag is created checkInnerHtml "div" "before|

inner2

|after" testWidget' preSwitchover check $ do replace <- triggerEventWithChan replaceChan el "div" $ do text "before|" _ <- runWithReplace (text "inner1") $ el "p" (text "inner2") <$ replace text "|after" session' "traverseDMapWithKeyWithAdjust" $ do let widget :: DomBuilder t m => DKey a -> Identity a -> m (Identity a) widget k (Identity v) = elAttr "li" ("id" =: textKey k) $ do elClass "span" "key" $ text $ textKey k elClass "span" "value" $ text $ T.pack $ has @Show k $ show v pure (Identity v) checkItem :: WD.Element -> Text -> Text -> WD () checkItem li k v = do putStrLnDebug "checkItem" shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key") shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value") checkInitialItems dm xs = do putStrLnDebug "checkInitialItems" liftIO $ assertEqual "Wrong amount of items in DOM" (DMap.size dm) (length xs) forM_ (zip xs (DMap.toList dm)) $ \(e, k :=> Identity v) -> checkItem e (textKey k) (T.pack $ has @Show k $ show v) getAndCheckInitialItems dm = withRetry $ do putStrLnDebug "getAndCheckInitialItems" xs <- WD.findElems (WD.ByTag "li") checkInitialItems dm xs pure xs checkRemoval chan k = do putStrLnDebug "checkRemoval" e <- findElemWithRetry (WD.ById $ textKey k) liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe Nothing) elementShouldBeRemoved e checkReplace chan k v = do putStrLnDebug "checkReplace" e <- findElemWithRetry (WD.ById $ textKey k) liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe $ Just $ Identity v) elementShouldBeRemoved e e' <- findElemWithRetry $ WD.ById $ textKey k checkItem e' (textKey k) (T.pack $ show v) checkInsert chan k v = do putStrLnDebug "checkInsert" liftIO $ writeChan chan $ PatchDMap $ DMap.singleton k (ComposeMaybe $ Just $ Identity v) e <- findElemWithRetry (WD.ById $ textKey k) checkItem e (textKey k) (T.pack $ show v) postBuildPatch = PatchDMap $ DMap.fromList [Key_Char :=> ComposeMaybe Nothing, Key_Bool :=> ComposeMaybe (Just $ Identity True)] it "doesn't replace elements at switchover, can delete/update/insert" $ runWD $ do chan <- liftIO newChan let preSwitchover :: WD [WD.Element] preSwitchover = getAndCheckInitialItems keyMap check :: [WD.Element] -> WD () check xs = do checkInitialItems keyMap xs checkRemoval chan Key_Int checkReplace chan Key_Char 'B' checkInsert chan Key_Bool True testWidget' preSwitchover check $ do (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap =<< triggerEventWithChan chan liftIO $ dmap `H.shouldBe` keyMap it "handles postBuild correctly" $ runWD $ do chan <- liftIO newChan let preSwitchover = getAndCheckInitialItems $ applyAlways postBuildPatch keyMap check xs = do withRetry $ checkInitialItems (applyAlways postBuildPatch keyMap) xs checkRemoval chan Key_Int checkInsert chan Key_Char 'B' checkReplace chan Key_Bool True testWidget' preSwitchover check $ void $ do pb <- getPostBuild replace <- triggerEventWithChan chan (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap $ leftmost [postBuildPatch <$ pb, replace] liftIO $ dmap `H.shouldBe` keyMap it "can delete/update/insert when built in prerender" $ runWD $ do chan <- liftIO newChan let check = do _ <- getAndCheckInitialItems keyMap checkRemoval chan Key_Int checkReplace chan Key_Char 'B' checkInsert chan Key_Bool True testWidget (pure ()) check $ do replace <- triggerEventWithChan chan prerender_ (pure ()) $ do (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap replace liftIO $ dmap `H.shouldBe` keyMap it "can delete/update/insert when built in immediate mode" $ runWD $ do chan <- liftIO newChan let check = do _ <- getAndCheckInitialItems keyMap checkRemoval chan Key_Int checkReplace chan Key_Char 'B' checkInsert chan Key_Bool True testWidget (pure ()) check $ void $ do pb <- getPostBuild runWithReplace (pure ()) $ ffor pb $ \() -> void $ do (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap =<< triggerEventWithChan chan liftIO $ dmap `H.shouldBe` keyMap -- Should be fixed by prerender changes! it "handles postBuild correctly in prerender" $ runWD $ do chan <- liftIO newChan let check = do _ <- getAndCheckInitialItems $ applyAlways postBuildPatch keyMap checkRemoval chan Key_Int checkInsert chan Key_Char 'B' checkReplace chan Key_Bool True testWidget (pure ()) check $ do replace <- triggerEventWithChan chan prerender_ (pure ()) $ void $ do pb <- getPostBuild (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap $ leftmost [postBuildPatch <$ pb, replace] liftIO $ dmap `H.shouldBe` keyMap session' "traverseIntMapWithKeyWithAdjust" $ do let textKeyInt k = "key" <> T.pack (show k) intMap = IntMap.fromList [ (1, "one") , (2, "two") , (3, "three") ] let widget :: DomBuilder t m => IntMap.Key -> Text -> m Text widget k v = elAttr "li" ("id" =: textKeyInt k) $ do elClass "span" "key" $ text $ textKeyInt k elClass "span" "value" $ text v pure v checkItem :: WD.Element -> Text -> Text -> WD () checkItem li k v = do putStrLnDebug "checkItem" shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key") shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value") checkInitialItems dm xs = do putStrLnDebug "checkInitialItems" liftIO $ assertEqual "Wrong amount of items in DOM" (IntMap.size dm) (length xs) forM_ (zip xs (IntMap.toList dm)) $ \(e, (k, v)) -> checkItem e (textKeyInt k) v getAndCheckInitialItems dm = withRetry $ do putStrLnDebug "getAndCheckInitialItems" xs <- WD.findElems (WD.ByTag "li") checkInitialItems dm xs pure xs checkRemoval chan k = do putStrLnDebug "checkRemoval" e <- findElemWithRetry (WD.ById $ textKeyInt k) liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k Nothing elementShouldBeRemoved e checkReplace chan k v = do putStrLnDebug "checkReplace" e <- findElemWithRetry (WD.ById $ textKeyInt k) liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k $ Just v elementShouldBeRemoved e e' <- findElemWithRetry (WD.ById $ textKeyInt k) withRetry $ checkItem e' (textKeyInt k) v checkInsert chan k v = do putStrLnDebug "checkInsert" liftIO $ writeChan chan $ PatchIntMap $ IntMap.singleton k $ Just v e <- findElemWithRetry (WD.ById $ textKeyInt k) checkItem e (textKeyInt k) v postBuildPatch = PatchIntMap $ IntMap.fromList [(2, Nothing), (3, Just "trois"), (4, Just "four")] xit "doesn't replace elements at switchover, can delete/update/insert" $ runWD $ do chan <- liftIO newChan let preSwitchover = getAndCheckInitialItems intMap check xs = do checkInitialItems intMap xs checkRemoval chan 1 checkReplace chan 2 "deux" checkInsert chan 4 "four" testWidget' preSwitchover check $ void $ do (im, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap =<< triggerEventWithChan chan liftIO $ im `H.shouldBe` intMap xit "handles postBuild correctly" $ runWD $ do chan <- liftIO newChan let preSwitchover = getAndCheckInitialItems $ applyAlways postBuildPatch intMap check xs = do withRetry $ checkInitialItems (applyAlways postBuildPatch intMap) xs checkRemoval chan 1 checkInsert chan 2 "deux" checkReplace chan 3 "trois" testWidget' preSwitchover check $ void $ do pb <- getPostBuild replace <- triggerEventWithChan chan (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap $ leftmost [postBuildPatch <$ pb, replace] liftIO $ dmap `H.shouldBe` intMap xit "can delete/update/insert when built in prerender" $ runWD $ do chan <- liftIO newChan let check = do _ <- getAndCheckInitialItems intMap checkRemoval chan 1 checkReplace chan 2 "deux" checkInsert chan 4 "quatre" testWidget (pure ()) check $ do replace <- triggerEventWithChan chan prerender_ (pure ()) $ do (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap replace liftIO $ dmap `H.shouldBe` intMap xit "can delete/update/insert when built in immediate mode" $ runWD $ do chan <- liftIO newChan let check = do _ <- getAndCheckInitialItems intMap checkRemoval chan 1 checkReplace chan 2 "deux" checkInsert chan 4 "quatre" testWidget (pure ()) check $ void $ do pb <- getPostBuild runWithReplace (pure ()) $ ffor pb $ \() -> void $ do (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap =<< triggerEventWithChan chan liftIO $ dmap `H.shouldBe` intMap -- Should be fixed by prerender changes! xit "handles postBuild correctly in prerender" $ runWD $ do chan <- liftIO newChan let check = do _ <- getAndCheckInitialItems $ applyAlways postBuildPatch intMap checkRemoval chan 1 checkInsert chan 2 "deux" checkReplace chan 3 "trois" testWidget (pure ()) check $ do replace <- triggerEventWithChan chan prerender_ (pure ()) $ void $ do pb <- getPostBuild (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap $ leftmost [postBuildPatch <$ pb, replace] liftIO $ dmap `H.shouldBe` intMap session' "traverseDMapWithKeyWithAdjustWithMove" $ do let widget :: DomBuilder t m => Key2 a -> Identity a -> m (Identity a) widget k (Identity v) = elAttr "li" ("id" =: textKey2 k) $ do elClass "span" "key" $ text $ textKey2 k elClass "span" "value" $ text $ T.pack $ has @Show k $ show v pure (Identity v) textKey2 :: Key2 a -> Text textKey2 = \case Key2_Int i -> "i" <> T.pack (show i) Key2_Char c -> "c" <> T.pack [c] checkItem :: WD.Element -> Text -> Text -> WD () checkItem li k v = do shouldContainTextNoRetry k =<< WD.findElemFrom li (WD.ByClass "key") shouldContainTextNoRetry v =<< WD.findElemFrom li (WD.ByClass "value") checkInitialItems dm xs = do liftIO $ assertEqual "Wrong amount of items in DOM" (DMap.size dm) (length xs) forM_ (zip xs (DMap.toList dm)) $ \(e, k :=> Identity v) -> checkItem e (textKey2 k) (T.pack $ has @Show k $ show v) getAndCheckInitialItems dm = withRetry $ do xs <- WD.findElems (WD.ByTag "li") checkInitialItems dm xs pure xs moveSpec :: (DMap Key2 Identity -> (WD.Element -> Chan (PatchDMapWithMove Key2 Identity) -> WD ()) -> WD ()) -> H.SpecM (WdTestSession ()) () moveSpec testMove = do it "can insert an item" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 3 ==> 3]) $ \body chan -> do shouldContainText (T.strip $ T.unlines ["i11","i33"]) body liftIO $ writeChan chan (insertDMapKey (Key2_Int 2) 2) shouldContainText (T.strip $ T.unlines ["i11","i22","i33"]) body it "can delete an item" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 2 ==> 2, Key2_Int 3 ==> 3]) $ \body chan -> do shouldContainText (T.strip $ T.unlines ["i11","i22","i33"]) body liftIO $ writeChan chan (deleteDMapKey (Key2_Int 2)) shouldContainText (T.strip $ T.unlines ["i11","i33"]) body it "can swap items" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 2 ==> 2, Key2_Int 3 ==> 3, Key2_Int 4 ==> 4]) $ \body chan -> do shouldContainText (T.strip $ T.unlines ["i11","i22","i33","i44"]) body liftIO $ writeChan chan (swapDMapKey (Key2_Int 2) (Key2_Int 3)) shouldContainText (T.strip $ T.unlines ["i11","i33","i22","i44"]) body -- TODO re-enable this -- it "can move items" $ runWD $ testMove (DMap.fromList [Key2_Int 1 ==> 1, Key2_Int 2 ==> 2, Key2_Int 3 ==> 3, Key2_Int 4 ==> 4]) $ \body chan -> do -- shouldContainText (T.strip $ T.unlines ["i11","i22","i33", "i44"]) body -- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3)) -- shouldContainText (T.strip $ T.unlines ["i11","i22","i44"]) body -- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3)) -- attempt a move to nonexistent key should delete -- shouldContainText (T.strip $ T.unlines ["i11","i44"]) body -- liftIO $ writeChan chan (moveDMapKey (Key2_Int 2) (Key2_Int 3)) -- this causes a JSException in immediate builder too -- shouldContainText (T.strip $ T.unlines ["i11","i44"]) body -- liftIO $ writeChan chan (insertDMapKey (Key2_Int 2) 2) -- this step will fail if the above caused an exception, thus works as a proxy for detecting the exception given we can't catch it -- shouldContainText (T.strip $ T.unlines ["i11","i22","i44"]) body describe "hydration" $ moveSpec $ \initMap test -> do chan <- liftIO newChan let preSwitchover = getAndCheckInitialItems initMap check xs = do checkInitialItems initMap xs body <- getBody test body chan testWidget' preSwitchover check $ void $ do (dmap, _evt) <- traverseDMapWithKeyWithAdjustWithMove widget initMap =<< triggerEventWithChan chan liftIO $ assertEqual "DMap" initMap dmap describe "hydration/immediate" $ moveSpec $ \initMap test -> do chan <- liftIO newChan replace <- liftIO newChan lock :: MVar () <- liftIO newEmptyMVar let check = do liftIO $ do writeChan replace () takeMVar lock xs <- getAndCheckInitialItems initMap checkInitialItems initMap xs body <- getBody test body chan testWidget (pure ()) check $ void $ do e <- triggerEventWithChan replace runWithReplace (pure ()) $ ffor e $ \() -> void $ do pb <- getPostBuild performEvent_ $ liftIO (putMVar lock ()) <$ pb (dmap, _evt) <- traverseDMapWithKeyWithAdjustWithMove widget initMap =<< triggerEventWithChan chan liftIO $ assertEqual "DMap" initMap dmap session' "hydrating invalid HTML" $ do xit "can hydrate list in paragraph" $ runWD $ do let preSwitchover = do checkBodyText "before\ninner\nafter" -- Two

tags should be present (p1, p2) <- WD.findElems (WD.ByTag "p") >>= \case [p1, p2] -> pure (p1, p2) _ -> error "Unexpected number of `p` tags (expected 2)" ol <- findElemWithRetry (WD.ByTag "ol") shouldContainText "before" p1 shouldContainText "inner" ol shouldContainText "" p2 pure (p1, ol, p2) check (p1, ol, p2) = do checkBodyText "before\ninner\nafter" shouldContainText "before\ninner\nafter" p1 elementShouldBeRemoved ol elementShouldBeRemoved p2 -- Don't fail fatally when hydration encounters the invalid DOM testWidgetDebug' False withDebugging preSwitchover check $ do -- This is deliberately invalid HTML, the browser will interpret it as --

before

    inner
after

el "p" $ do text "before" el "ol" $ text "inner" text "after" -- TODO: This test presupposes the exact set of labels that "dropdown" places in the "value" fields to distinguish options. -- This dependence on internal implementation details is undesirable in a test case, but seems fairly tricky to avoid. -- It seems expedient for the time being to expect this test case to be updated, should those implementation details ever change. session' "dropdown" $ do let doTest expectedOpts (initialValue :: Text) = do let doCheck = do es <- findElemsWithRetry $ WD.ByTag "option" opts <- mapM fetchElement es assertEqual "missing/extra/incorrect option element(s)" expectedOpts (sort opts) testWidget doCheck doCheck $ do void $ dropdown initialValue (constDyn (("aa" :: Text) =: "aaa" <> "bb" =: "bbb")) def fetchElement e = do val <- WD.attr e "value" sel <- WD.attr e "selected" return (fromMaybe "" val, isJust sel) -- The "aa" test case is important, but a good test implementation probably needs to avoid selenium, because HTML parsers will insert a "selected" attribute on the first "option" tag if no selected attributes are present; thus as written, this erroneously succeeds on the old implementation (but properly implemented, should fail) -- Thus, it would appear that we do actually need a HTML5 or maybe XML parser for this test suite. xit "statically renders initial values (on aa)" $ runWD $ do doTest [("0",True),("1",False)] "aa" -- These tests are currently marked "pending" (by using "xit" instead of "it") because this test has a high chance of non-deterministically failing, which is a problem elsewhere in this test suite as well xit "statically renders initial values (on bb)" $ runWD $ do doTest [("0",False),("1",True)] "bb" data Selenium = Selenium { _selenium_portNumber :: PortNumber , _selenium_stopServer :: IO () } startSeleniumServer :: PortNumber -> IO (IO ()) startSeleniumServer port = do (_,_,_,ph) <- createProcess $ (proc "selenium-server" ["-port", show port]) { std_in = NoStream , std_out = NoStream , std_err = NoStream } return $ terminateProcess ph withSeleniumServer :: (Selenium -> IO ()) -> IO () withSeleniumServer f = do stopServer <- startSeleniumServer seleniumPort threadDelay $ 1000 * 1000 * 2 -- TODO poll or wait on a a signal to block on f $ Selenium { _selenium_portNumber = seleniumPort , _selenium_stopServer = stopServer } triggerEventWithChan :: (Reflex t, TriggerEvent t m, Prerender js t m) => Chan a -> m (Event t a) triggerEventWithChan chan = do (e, trigger) <- newTriggerEvent -- In prerender because we only want to do this on the client prerender_ (pure ()) $ void $ liftIO $ forkIO $ forever $ trigger =<< readChan chan pure e shouldBeWithRetryM :: (Eq a, Show a) => WD a -> a -> WD () shouldBeWithRetryM m expected = withRetry $ do got <- m got `shouldBe` expected assertAttr :: WD.Element -> Text -> Maybe Text -> WD () assertAttr e k v = liftIO . assertEqual "Incorrect attribute value" v =<< WD.attr e k elementShouldBeRemoved :: WD.Element -> WD () elementShouldBeRemoved e = withRetry $ do try (WD.getText e) >>= \case Left (WD.FailedCommand WD.StaleElementReference _) -> return () Left err -> throwM err Right !_ -> liftIO $ assertFailure "Expected element to be removed, but it still exists" shouldContainText :: Text -> WD.Element -> WD () shouldContainText t = withRetry . shouldContainTextNoRetry t shouldContainTextNoRetry :: Text -> WD.Element -> WD () shouldContainTextNoRetry t = flip shouldBe t <=< WD.getText checkBodyText :: Text -> WD () checkBodyText = checkTextInTag "body" checkTextInTag :: Text -> Text -> WD () checkTextInTag t expected = do e <- findElemWithRetry (WD.ByTag t) shouldContainText expected e checkTextInId :: Text -> Text -> WD () checkTextInId i expected = do e <- findElemWithRetry (WD.ById i) shouldContainText expected e findElemWithRetry :: Selector -> WD WD.Element findElemWithRetry = withRetry . WD.findElem findElemsWithRetry :: Selector -> WD [WD.Element] findElemsWithRetry = withRetry . WD.findElems getBody :: WD WD.Element getBody = WD.findElem $ WD.ByTag "body" withRetry :: forall a. WD a -> WD a withRetry a = wait 300 where wait :: Int -> WD a wait 0 = a wait n = try a >>= \case Left (e :: SomeException) -> do liftIO $ putStrLn ("(retrying due to " <> show e <> ")") *> threadDelay 100000 wait $ n - 1 Right v -> return v divId :: DomBuilder t m => Text -> m a -> m a divId i = elAttr "div" ("id" =: i) type TestWidget js t m = (DomBuilder t m, MonadHold t m, PostBuild t m, Prerender js t m, PerformEvent t m, TriggerEvent t m, MonadFix m, MonadIO (Performable m), MonadIO m) testWidgetStaticDebug :: Bool -> WD b -- ^ Webdriver commands to run before JS runs and after hydration switchover -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -- ^ Widget we are testing -> WD b testWidgetStaticDebug withDebugging w = testWidgetDebug True withDebugging (void w) w -- | TODO: do something about JSExceptions not causing tests to fail testWidgetDebug :: Bool -> Bool -> WD () -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page) -> WD b -- ^ Webdriver commands to run after hydration switchover -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -- ^ Widget we are testing -> WD b testWidgetDebug hardFailure withDebugging beforeJS afterSwitchover = testWidgetDebug' hardFailure withDebugging beforeJS (const afterSwitchover) data HydrationFailedException = HydrationFailedException deriving Show instance Exception HydrationFailedException -- | TODO: do something about JSExceptions not causing tests to fail testWidgetDebug' :: Bool -> Bool -> WD a -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page) -> (a -> WD b) -- ^ Webdriver commands to run after hydration switchover -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -- ^ Widget we are testing (contents of body) -> WD b testWidgetDebug' hardFailure withDebugging beforeJS afterSwitchover bodyWidget = do let putStrLnDebug :: MonadIO m => Text -> m () putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m staticApp = do el "head" $ pure () el "body" $ do bodyWidget el "script" $ text $ TE.decodeUtf8 $ LBS.toStrict $ jsaddleJs False putStrLnDebug "rendering static" ((), html) <- liftIO $ renderStatic $ runHydratableT staticApp putStrLnDebug "rendered static" waitBeforeJS <- liftIO newEmptyMVar -- Empty until JS should be run onFailure <- if hardFailure then (`throwTo` HydrationFailedException) <$> liftIO myThreadId else pure $ pure () waitUntilSwitchover <- liftIO newEmptyMVar -- Empty until switchover let entryPoint = do putStrLnDebug "taking waitBeforeJS" liftIO $ takeMVar waitBeforeJS let switchOverAction = do putStrLnDebug "switchover syncPoint" syncPoint putStrLnDebug "putting waitUntilSwitchover" liftIO $ putMVar waitUntilSwitchover () putStrLnDebug "put waitUntilSwitchover" putStrLnDebug "running mainHydrationWidgetWithSwitchoverAction" mainHydrationWidgetWithSwitchoverActionWithFailure' onFailure switchOverAction blank bodyWidget putStrLnDebug "syncPoint after mainHydrationWidgetWithSwitchoverAction" syncPoint application <- liftIO $ jsaddleOr defaultConnectionOptions entryPoint $ \_ sendResponse -> do putStrLnDebug "sending response" r <- sendResponse $ responseLBS status200 [] $ "\n" <> LBS.fromStrict html putStrLnDebug "sent response" return r waitJSaddle <- liftIO newEmptyMVar let settings = foldr ($) Warp.defaultSettings [ Warp.setPort $ fromIntegral $ toInteger jsaddlePort , Warp.setBeforeMainLoop $ do putStrLnDebug "putting waitJSaddle" putMVar waitJSaddle () putStrLnDebug "put waitJSaddle" ] -- hSilence to get rid of ConnectionClosed logs silenceIfDebug = if withDebugging then id else hSilence [stderr] jsaddleWarp = silenceIfDebug $ Warp.runSettings settings application withAsync' jsaddleWarp $ do putStrLnDebug "taking waitJSaddle" liftIO $ takeMVar waitJSaddle putStrLnDebug "opening page" WD.openPage $ "http://localhost:" <> show jsaddlePort putStrLnDebug "running beforeJS" a <- beforeJS putStrLnDebug "putting waitBeforeJS" liftIO $ putMVar waitBeforeJS () putStrLnDebug "taking waitUntilSwitchover" liftIO $ takeMVar waitUntilSwitchover putStrLnDebug "running afterSwitchover" afterSwitchover a withAsync' :: (MonadIO m, MonadMask m) => IO a -> m b -> m b withAsync' f g = bracket (liftIO $ Async.async f) (liftIO . Async.uninterruptibleCancel) (const g) data Key2 a where Key2_Int :: Int -> Key2 Int Key2_Char :: Char -> Key2 Char deriveGEq ''Key2 deriveGCompare ''Key2 deriveGShow ''Key2 deriveArgDict ''Key2