@@ -1,13 +1,9 @@
1−{-# OPTIONS_GHC -fno-warn-orphans #-}
2{-# LANGUAGE BangPatterns #-}
1{-# LANGUAGE BangPatterns #-}
3−{-# LANGUAGE StandaloneDeriving #-}
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5−{-# LANGUAGE UndecidableInstances #-}
6{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE ConstraintKinds #-}
8{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleContexts #-}
9{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
10{-# LANGUAGE GADTs #-}
5{-# LANGUAGE GADTs #-}
6+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
11{-# LANGUAGE LambdaCase #-}
7{-# LANGUAGE LambdaCase #-}
12{-# LANGUAGE MultiParamTypeClasses #-}
8{-# LANGUAGE MultiParamTypeClasses #-}
13{-# LANGUAGE OverloadedStrings #-}
9{-# LANGUAGE OverloadedStrings #-}
@@ -15,13 +11,27 @@
15{-# LANGUAGE RankNTypes #-}
11{-# LANGUAGE RankNTypes #-}
16{-# LANGUAGE RecursiveDo #-}
12{-# LANGUAGE RecursiveDo #-}
17{-# LANGUAGE ScopedTypeVariables #-}
13{-# LANGUAGE ScopedTypeVariables #-}
14+{-# LANGUAGE StandaloneDeriving #-}
18{-# LANGUAGE TemplateHaskell #-}
15{-# LANGUAGE TemplateHaskell #-}
19{-# LANGUAGE TupleSections #-}
16{-# LANGUAGE TupleSections #-}
20{-# LANGUAGE TypeApplications #-}
17{-# LANGUAGE TypeApplications #-}
21{-# LANGUAGE TypeFamilies #-}
18{-# LANGUAGE TypeFamilies #-}
19+{-# LANGUAGE UndecidableInstances #-}
21+{-# OPTIONS_GHC -fno-warn-orphans #-}
23+-- Notice to those working on this test suite:
24+-- It doesn't appear to be possible to use --match to run a particular test, in
25+-- this situation the test may hang before reaching our code. This seems to
26+-- happen when the test is not the first test in the block. As a workaround, you
27+-- can just comment out the other tests. Also, using `xit` will cause the same
28+-- issue. `xit` tests must be the last tests in the session, or the session will
29+-- hang in the following test.
23import Prelude hiding (fail)
31import Prelude hiding (fail)
24import Control.Concurrent
32import Control.Concurrent
33+import qualified Control.Concurrent.Async as Async
34+import Control.Lens.Operators
25import Control.Monad hiding (fail)
35import Control.Monad hiding (fail)
26import Control.Monad.Catch
36import Control.Monad.Catch
27import Control.Monad.Fail
37import Control.Monad.Fail
@@ -31,12 +41,13 @@ import Control.Monad.Ref
31import Data.Constraint.Extras
41import Data.Constraint.Extras
32import Data.Constraint.Extras.TH
42import Data.Constraint.Extras.TH
33import Data.Dependent.Map (DMap)
43import Data.Dependent.Map (DMap)
34−import Data.Dependent.Sum (DSum(..), (==>), EqTag(..), ShowTag(..))
44+import Data.Dependent.Sum (DSum(..), (==>))
35import Data.Functor.Identity
45import Data.Functor.Identity
36import Data.Functor.Misc
46import Data.Functor.Misc
37import Data.GADT.Compare.TH
47import Data.GADT.Compare.TH
38import Data.GADT.Show.TH
48import Data.GADT.Show.TH
39import Data.IORef (IORef)
49import Data.IORef (IORef)
50+import Data.List (sort)
40import Data.Maybe
51import Data.Maybe
41import Data.Proxy
52import Data.Proxy
42import Data.Text (Text)
53import Data.Text (Text)
@@ -47,6 +58,7 @@ import Network.Socket
47import Network.Wai
58import Network.Wai
48import Network.WebSockets
59import Network.WebSockets
49import Reflex.Dom.Core
60import Reflex.Dom.Core
61+import Reflex.Dom.Widget.Input (dropdown)
50import Reflex.Patch.DMapWithMove
62import Reflex.Patch.DMapWithMove
51import System.Directory
63import System.Directory
52import System.Environment
64import System.Environment
@@ -55,14 +67,13 @@ import System.IO.Silently
55import System.IO.Temp
67import System.IO.Temp
56import System.Process
68import System.Process
57import System.Which (staticWhich)
69import System.Which (staticWhich)
58−import qualified Test.HUnit as HUnit (assertEqual, assertFailure)
70+import qualified Test.HUnit as HUnit
59import qualified Test.Hspec as H
71import qualified Test.Hspec as H
60import qualified Test.Hspec.Core.Spec as H
72import qualified Test.Hspec.Core.Spec as H
61import Test.Hspec (xit)
73import Test.Hspec (xit)
62import Test.Hspec.WebDriver hiding (runWD, click, uploadFile, WD)
74import Test.Hspec.WebDriver hiding (runWD, click, uploadFile, WD)
63import qualified Test.Hspec.WebDriver as WD
75import qualified Test.Hspec.WebDriver as WD
64import Test.WebDriver (WD(..))
76import Test.WebDriver (WD(..))
65−import Test.WebDriver.Exceptions (ServerError(..))
67import qualified Data.ByteString.Lazy as LBS
78import qualified Data.ByteString.Lazy as LBS
68import qualified Data.Dependent.Map as DMap
79import qualified Data.Dependent.Map as DMap
@@ -78,6 +89,9 @@ import qualified Test.WebDriver.Capabilities as WD
78import Test.Util.ChromeFlags
89import Test.Util.ChromeFlags
79import Test.Util.UnshareNetwork
90import Test.Util.UnshareNetwork
92+-- ORPHAN: https://github.com/kallisti-dev/hs-webdriver/pull/167
93+deriving instance MonadMask WD
81chromium :: FilePath
95chromium :: FilePath
82chromium = $(staticWhich "chromium")
96chromium = $(staticWhich "chromium")
@@ -93,11 +107,14 @@ instance MonadRef WD where
93 writeRef r = WD . writeRef r
107 writeRef r = WD . writeRef r
95assertEqual :: (MonadIO m, Eq a, Show a) => String -> a -> a -> m ()
109assertEqual :: (MonadIO m, Eq a, Show a) => String -> a -> a -> m ()
96−assertEqual a b = liftIO . HUnit.assertEqual a b
110+assertEqual msg a b = liftIO $ HUnit.assertEqual msg a b
98assertFailure :: MonadIO m => String -> m ()
112assertFailure :: MonadIO m => String -> m ()
99assertFailure = liftIO . HUnit.assertFailure
113assertFailure = liftIO . HUnit.assertFailure
115+assertBool :: (MonadIO m) => String -> Bool -> m ()
116+assertBool msg bool = liftIO $ HUnit.assertBool msg bool
101chromeConfig :: Text -> [Text] -> WD.WDConfig
118chromeConfig :: Text -> [Text] -> WD.WDConfig
102chromeConfig fp flags = WD.useBrowser (WD.chrome { WD.chromeBinary = Just $ T.unpack fp, WD.chromeOptions = T.unpack <$> flags }) WD.defaultConfig
119chromeConfig fp flags = WD.useBrowser (WD.chrome { WD.chromeBinary = Just $ T.unpack fp, WD.chromeOptions = T.unpack <$> flags }) WD.defaultConfig
@@ -128,7 +145,7 @@ deriving instance MonadFail WD
129main :: IO ()
146main :: IO ()
131− unshareNetork
148+ unshareNetwork
132 isHeadless <- (== Nothing) <$> lookupEnv "NO_HEADLESS"
149 isHeadless <- (== Nothing) <$> lookupEnv "NO_HEADLESS"
133 withSandboxedChromeFlags isHeadless $ \chromeFlags -> do
150 withSandboxedChromeFlags isHeadless $ \chromeFlags -> do
134 withSeleniumServer $ \selenium -> do
151 withSeleniumServer $ \selenium -> do
@@ -137,13 +154,13 @@ main = do
137 withDebugging <- isNothing <$> lookupEnv "NO_DEBUG"
154 withDebugging <- isNothing <$> lookupEnv "NO_DEBUG"
138 let wdConfig = WD.defaultConfig { WD.wdPort = fromIntegral $ _selenium_portNumber selenium }
155 let wdConfig = WD.defaultConfig { WD.wdPort = fromIntegral $ _selenium_portNumber selenium }
139 chromeCaps' = WD.getCaps $ chromeConfig browserPath chromeFlags
156 chromeCaps' = WD.getCaps $ chromeConfig browserPath chromeFlags
140− hspec (tests withDebugging wdConfig [chromeCaps'] selenium) `finally` _selenium_stopServer selenium
157+ hspec (tests withDebugging wdConfig [(chromeCaps', "chrome")] selenium) `finally` _selenium_stopServer selenium
142−tests :: Bool -> WD.WDConfig -> [Capabilities] -> Selenium -> Spec
159+tests :: Bool -> WD.WDConfig -> [(Capabilities, String)] -> Selenium -> Spec
143tests withDebugging wdConfig caps _selenium = do
160tests withDebugging wdConfig caps _selenium = do
144 let putStrLnDebug :: MonadIO m => Text -> m ()
161 let putStrLnDebug :: MonadIO m => Text -> m ()
145 putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
162 putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
146− session' = sessionWith wdConfig "" . using (map (,"") caps)
163+ session' t = sessionWith wdConfig t . using caps
147 runWD m = runWDOptions (WdOptions False) $ do
164 runWD m = runWDOptions (WdOptions False) $ do
148 putStrLnDebug "before"
165 putStrLnDebug "before"
@@ -152,10 +169,10 @@ tests withDebugging wdConfig caps _selenium = do
152 testWidgetStatic :: WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
169 testWidgetStatic :: WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
153 testWidgetStatic = testWidgetStaticDebug withDebugging
170 testWidgetStatic = testWidgetStaticDebug withDebugging
154 testWidget :: WD () -> WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
171 testWidget :: WD () -> WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
155− testWidget = testWidgetDebug withDebugging
172+ testWidget = testWidgetDebug True withDebugging
156 testWidget' :: WD a -> (a -> WD b) -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
173 testWidget' :: WD a -> (a -> WD b) -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
157− testWidget' = testWidgetDebug' withDebugging
174+ testWidget' = testWidgetDebug' True withDebugging
158− describe "text" $ session' $ do
175+ session' "text" $ do
159 it "works" $ runWD $ do
176 it "works" $ runWD $ do
160 testWidgetStatic (checkBodyText "hello world") $ do
177 testWidgetStatic (checkBodyText "hello world") $ do
161 text "hello world"
178 text "hello world"
@@ -216,7 +233,7 @@ tests withDebugging wdConfig caps _selenium = do
216 click <- button ""
233 click <- button ""
217 void $ textNode $ TextNodeConfig "initial" $ Just $ "after" <$ click
234 void $ textNode $ TextNodeConfig "initial" $ Just $ "after" <$ click
219− describe "element" $ session' $ do
236+ session' "element" $ do
220 it "works with domEvent Click" $ runWD $ do
237 it "works with domEvent Click" $ runWD $ do
221 clickedRef <- liftIO $ newRef False
238 clickedRef <- liftIO $ newRef False
222 testWidget' (findElemWithRetry $ WD.ByTag "div") WD.click $ do
239 testWidget' (findElemWithRetry $ WD.ByTag "div") WD.click $ do
@@ -280,21 +297,78 @@ tests withDebugging wdConfig caps _selenium = do
280 let click = domEvent Click e
297 let click = domEvent Click e
281 return ()
298 return ()
283− describe "inputElement" $ do
300+ session' "inputElement" $ do
301+ describe "static renderer" $ do
302+ it "sets value attribute" $ runWD $ do
303+ let checkStatic = do
304+ e <- findElemWithRetry $ WD.ByTag "input"
305+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
307+ checkHydrated e = do
308+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
309+ testWidget' checkStatic checkHydrated $ void $ inputElement $ def
310+ & inputElementConfig_initialValue .~ "test"
311+ it "updates value attribute at postBuild" $ runWD $ do
312+ let checkStatic = do
313+ e <- findElemWithRetry $ WD.ByTag "input"
314+ WD.attr e "value" `shouldBeWithRetryM` Just "test-updated"
316+ checkHydrated e = do
317+ WD.attr e "value" `shouldBeWithRetryM` Just "test-updated"
318+ testWidget' checkStatic checkHydrated $ do
320+ _ <- inputElement $ def
321+ & inputElementConfig_initialValue .~ "test"
322+ & inputElementConfig_setValue .~ ("test-updated" <$ pb)
324+ it "sets checked attr appropriately" $ runWD $ do
325+ setCheckedChan <- liftIO newChan
326+ let checkStatic = do
327+ e <- findElemWithRetry $ WD.ByTag "input"
328+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
331+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
333+ WD.click e -- Click to uncheck
334+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
335+ liftIO $ writeChan setCheckedChan True -- Programatically check the checkbox
336+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
337+ testWidget' checkStatic checkValue $ do
338+ setChecked <- triggerEventWithChan setCheckedChan
339+ _ <- inputElement $ def
340+ & initialAttributes .~ "type" =: "checkbox"
341+ & inputElementConfig_initialChecked .~ True
342+ & inputElementConfig_setChecked .~ setChecked
344+ it "sets checked attr appropriately at postbuild" $ runWD $ do
345+ let checkStatic = do
346+ e <- findElemWithRetry $ WD.ByTag "input"
347+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
350+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
351+ testWidget' checkStatic checkValue $ do
353+ _ <- inputElement $ def
354+ & initialAttributes .~ "type" =: "checkbox"
355+ & inputElementConfig_initialChecked .~ False
356+ & inputElementConfig_setChecked .~ (True <$ pb)
284− describe "hydration" $ session' $ do
358+ describe "hydration" $ do
285 it "doesn't wipe user input when switching over" $ runWD $ do
359 it "doesn't wipe user input when switching over" $ runWD $ do
286− inputRef <- newRef ("" :: Text)
360+ inputRef <- newRef ("hello " :: Text)
287 testWidget'
361 testWidget'
289 e <- findElemWithRetry $ WD.ByTag "input"
363 e <- findElemWithRetry $ WD.ByTag "input"
290− WD.sendKeys "hello world" e
364+ WD.sendKeys "world" e
292 (\e -> do
366 (\e -> do
293 WD.attr e "value" `shouldBeWithRetryM` Just "hello world"
367 WD.attr e "value" `shouldBeWithRetryM` Just "hello world"
294 WD.click =<< findElemWithRetry (WD.ByTag "button")
368 WD.click =<< findElemWithRetry (WD.ByTag "button")
295 readRef inputRef `shouldBeWithRetryM` "hello world"
369 readRef inputRef `shouldBeWithRetryM` "hello world"
297− e <- inputElement def
371+ e <- inputElement $ def & inputElementConfig_initialValue .~ "hello "
298 click <- button "save"
372 click <- button "save"
299 performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
373 performEvent_ $ liftIO . writeRef inputRef <$> tag (current (value e)) click
300 it "captures user input after switchover" $ runWD $ do
374 it "captures user input after switchover" $ runWD $ do
@@ -351,24 +425,49 @@ tests withDebugging wdConfig caps _selenium = do
351 checkedByUIRef <- newRef False
425 checkedByUIRef <- newRef False
352 checkedRef <- newRef False
426 checkedRef <- newRef False
353 setCheckedChan <- liftIO newChan
427 setCheckedChan <- liftIO newChan
428+ let checkStatic = do
429+ e <- findElemWithRetry $ WD.ByTag "input"
430+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
354− let checkValue = do
432+ checkValue e = do
355 readRef checkedByUIRef `shouldBeWithRetryM` False
433 readRef checkedByUIRef `shouldBeWithRetryM` False
356 readRef checkedRef `shouldBeWithRetryM` False
434 readRef checkedRef `shouldBeWithRetryM` False
357− e <- findElemWithRetry $ WD.ByTag "input"
435+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
358 WD.moveToCenter e
436 WD.moveToCenter e
359 WD.click e
437 WD.click e
360 readRef checkedByUIRef `shouldBeWithRetryM` True
438 readRef checkedByUIRef `shouldBeWithRetryM` True
361 readRef checkedRef `shouldBeWithRetryM` True
439 readRef checkedRef `shouldBeWithRetryM` True
440+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
362 liftIO $ writeChan setCheckedChan False
441 liftIO $ writeChan setCheckedChan False
363 readRef checkedByUIRef `shouldBeWithRetryM` True
442 readRef checkedByUIRef `shouldBeWithRetryM` True
364 readRef checkedRef `shouldBeWithRetryM` False
443 readRef checkedRef `shouldBeWithRetryM` False
444+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
365− testWidget (pure ()) checkValue $ do
445+ testWidget' checkStatic checkValue $ do
366 setChecked <- triggerEventWithChan setCheckedChan
446 setChecked <- triggerEventWithChan setCheckedChan
367 e <- inputElement $ def
447 e <- inputElement $ def
368 & initialAttributes .~ "type" =: "checkbox"
448 & initialAttributes .~ "type" =: "checkbox"
369 & inputElementConfig_setChecked .~ setChecked
449 & inputElementConfig_setChecked .~ setChecked
370 performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
450 performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
371 performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
451 performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
452+ it "respects user updates to checked which happen before hydration" $ runWD $ do
453+ checkedByUIRef <- newRef False
454+ checkedRef <- newRef False
455+ let checkStatic = do
456+ e <- findElemWithRetry $ WD.ByTag "input"
457+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
460+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
463+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
464+ readRef checkedByUIRef `shouldBeWithRetryM` True
465+ readRef checkedRef `shouldBeWithRetryM` True
466+ testWidget' checkStatic checkValue $ do
467+ e <- inputElement $ def
468+ & initialAttributes .~ "type" =: "checkbox"
469+ performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
470+ performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
372 it "captures file uploads" $ runWD $ do
471 it "captures file uploads" $ runWD $ do
373 filesRef :: IORef [Text] <- newRef []
472 filesRef :: IORef [Text] <- newRef []
374 let uploadFile = do
473 let uploadFile = do
@@ -384,8 +483,117 @@ tests withDebugging wdConfig caps _selenium = do
384 prerender_ (pure ()) $ performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do
483 prerender_ (pure ()) $ performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do
385 names <- liftJSM $ traverse File.getName fs
484 names <- liftJSM $ traverse File.getName fs
386 liftIO $ writeRef filesRef names
485 liftIO $ writeRef filesRef names
387−
486+ it "fires _input event if the user altered the value before hydration" $ runWD $ do
487+ input <- newRef ("" :: Text)
488+ update <- newRef ("" :: Text)
489+ let checkStatic = do
490+ e <- findElemWithRetry $ WD.ByTag "input"
491+ WD.attr e "value" `shouldBeWithRetryM` Just ""
492+ WD.sendKeys "test" e
493+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
495+ checkHydrated e = do
496+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
497+ readRef input `shouldBeWithRetryM` "test"
498+ readRef update `shouldBeWithRetryM` "test"
499+ testWidget' checkStatic checkHydrated $ do
500+ e <- inputElement def
501+ performEvent_ $ liftIO . writeRef input <$> _inputElement_input e
502+ performEvent_ $ liftIO . writeRef update <$> updated (_inputElement_value e)
503+ it "does not fire _input event when the value is updated at postBuild" $ runWD $ do
504+ input <- newRef (Nothing :: Maybe Text)
505+ let checkStatic = do
506+ e <- findElemWithRetry $ WD.ByTag "input"
507+ WD.attr e "value" `shouldBeWithRetryM` Just "pb"
509+ checkHydrated e = do
510+ WD.attr e "value" `shouldBeWithRetryM` Just "pb"
511+ readRef input `shouldBeWithRetryM` Nothing
512+ testWidget' checkStatic checkHydrated $ do
514+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
515+ performEvent_ $ liftIO . writeRef input . Just <$> _inputElement_input e
516+ it "SSR produces correct DOM based on inputElement values when setValue happens at postBuild" $ runWD $ do
518+ input <- findElemWithRetry $ WD.ByTag "input"
519+ WD.attr input "value" `shouldBeWithRetryM` Just "pb"
520+ p <- findElemWithRetry (WD.ByTag "p")
521+ shouldContainText "pb" p
522+ testWidget checkBoth checkBoth $ do
524+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
525+ el "p" $ dynText $ _inputElement_value e
526+ it "does not fail when both setValue AND user updated value happen before switchover" $ runWD $ do
527+ let checkStatic = do
528+ input <- findElemWithRetry $ WD.ByTag "input"
529+ h2_value <- findElemWithRetry (WD.ByTag "h2")
530+ h3_input <- findElemWithRetry (WD.ByTag "h3")
531+ WD.attr input "value" `shouldBeWithRetryM` Just "pb"
532+ shouldContainText "pb" h2_value
533+ shouldContainText "" h3_input
534+ WD.sendKeys "abc" input
535+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
536+ shouldContainText "pb" h2_value
537+ shouldContainText "" h3_input
538+ pure (input, h2_value, h3_input)
539+ checkHydrated (input, h2_value, h3_input) = do
540+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
541+ shouldContainText "pbabc" h3_input
542+ shouldContainText "pbabc pb" h2_value
544+ testWidget' checkStatic checkHydrated $ do
546+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
547+ el "h1" $ dynText $ _inputElement_value e
548+ el "h2" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ updated $ _inputElement_value e
549+ el "h3" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ _inputElement_input e
550+ it "value is correct when both setValue AND user updated value happen before switchover" $ runWD $ do
551+ let checkStatic = do
552+ input <- findElemWithRetry $ WD.ByTag "input"
553+ p <- findElemWithRetry (WD.ByTag "p")
554+ WD.attr input "value" `shouldBeWithRetryM` Just "pb"
555+ shouldContainText "pb" p
556+ WD.sendKeys "abc" input
557+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
558+ shouldContainText "pb" p -- It won't be updated yet
560+ checkHydrated (input, p) = do
561+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
562+ shouldContainText "pbabc" p
564+ testWidget' checkStatic checkHydrated $ do
566+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
567+ el "p" $ dynText $ _inputElement_value e
568+ it "input event and (updated value) fire correctly when both setValue AND user updated value happen before switchover" $ runWD $ do
569+ valRef <- newRef ([] :: [Text])
570+ inputRef <- newRef ([] :: [Text])
571+ let consRef ref a = liftIO $ atomicModifyRef ref $ \as -> (a:as, ())
573+ input <- findElemWithRetry $ WD.ByTag "input"
574+ p <- findElemWithRetry (WD.ByTag "p")
575+ WD.attr input "value" `shouldBeWithRetryM` Just "pb"
576+ shouldContainText "pb" p
577+ WD.sendKeys "abc" input
578+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
579+ shouldContainText "pb" p -- It won't be updated yet
580+ readRef inputRef `shouldBeWithRetryM` [] -- Should never fire input ref during SSR
581+ readRef valRef `shouldBeWithRetryM` ["pb"]
583+ checkHydrated (input, p) = do
584+ readRef inputRef `shouldBeWithRetryM` ["pbabc"]
585+ readRef valRef `shouldBeWithRetryM` ["pbabc", "pb"]
586+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
587+ shouldContainText "pbabc" p
589+ testWidget' checkStatic checkHydrated $ do
590+ liftIO $ writeRef valRef []
592+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
593+ el "p" $ dynText $ _inputElement_value e
594+ performEvent_ $ consRef valRef <$> updated (_inputElement_value e)
595+ performEvent_ $ consRef inputRef <$> _inputElement_input e
388− describe "hydration/immediate" $ session' $ do
596+ describe "hydration/immediate" $ do
389 it "captures user input after switchover" $ runWD $ do
597 it "captures user input after switchover" $ runWD $ do
390 inputRef :: IORef Text <- newRef ""
598 inputRef :: IORef Text <- newRef ""
391 let checkValue = do
599 let checkValue = do
@@ -465,8 +673,31 @@ tests withDebugging wdConfig caps _selenium = do
465 names <- liftJSM $ traverse File.getName fs
673 names <- liftJSM $ traverse File.getName fs
466 liftIO $ writeRef filesRef names
674 liftIO $ writeRef filesRef names
468− describe "textAreaElement" $ do
676+ session' "textAreaElement" $ do
469− describe "hydration" $ session' $ do
677+ describe "static renderer" $ do
678+ it "sets value attribute" $ runWD $ do
679+ let checkStatic = do
680+ e <- findElemWithRetry $ WD.ByTag "textarea"
681+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
683+ checkHydrated e = do
684+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
685+ testWidget' checkStatic checkHydrated $ void $ textAreaElement $ def
686+ & textAreaElementConfig_initialValue .~ "test"
687+ it "updates value attribute at postBuild" $ runWD $ do
688+ let checkStatic = do
689+ e <- findElemWithRetry $ WD.ByTag "textarea"
690+ WD.attr e "value" `shouldBeWithRetryM` Just "test-updated"
692+ checkHydrated e = do
693+ WD.attr e "value" `shouldBeWithRetryM` Just "test-updated"
694+ testWidget' checkStatic checkHydrated $ do
696+ _ <- textAreaElement $ def
697+ & textAreaElementConfig_initialValue .~ "test"
698+ & textAreaElementConfig_setValue .~ ("test-updated" <$ pb)
700+ describe "hydration" $ do
470 it "doesn't wipe user input when switching over" $ runWD $ do
701 it "doesn't wipe user input when switching over" $ runWD $ do
471 inputRef <- newRef ("" :: Text)
702 inputRef <- newRef ("" :: Text)
472 testWidget'
703 testWidget'
@@ -532,8 +763,90 @@ tests withDebugging wdConfig caps _selenium = do
532 e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' }
763 e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' }
533 performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
764 performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
534 performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
765 performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
766+ it "fires _input event if the user altered the value before hydration" $ runWD $ do
767+ textarea <- newRef ("" :: Text)
768+ update <- newRef ("" :: Text)
769+ let checkStatic = do
770+ e <- findElemWithRetry $ WD.ByTag "textarea"
771+ WD.attr e "value" `shouldBeWithRetryM` Just ""
772+ WD.sendKeys "test" e
773+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
775+ checkHydrated e = do
776+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
777+ readRef textarea `shouldBeWithRetryM` "test"
778+ readRef update `shouldBeWithRetryM` "test"
779+ testWidget' checkStatic checkHydrated $ do
780+ e <- textAreaElement def
781+ performEvent_ $ liftIO . writeRef textarea <$> _textAreaElement_input e
782+ performEvent_ $ liftIO . writeRef update <$> updated (_textAreaElement_value e)
783+ it "does not fire _input event when the value is updated at postBuild" $ runWD $ do
784+ textarea <- newRef (Nothing :: Maybe Text)
785+ let checkStatic = do
786+ e <- findElemWithRetry $ WD.ByTag "textarea"
787+ WD.attr e "value" `shouldBeWithRetryM` Just "pb"
789+ checkHydrated e = do
790+ WD.attr e "value" `shouldBeWithRetryM` Just "pb"
791+ readRef textarea `shouldBeWithRetryM` Nothing
792+ testWidget' checkStatic checkHydrated $ do
794+ e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb)
795+ performEvent_ $ liftIO . writeRef textarea . Just <$> _textAreaElement_input e
796+ it "SSR produces correct DOM based on textAreaElement values when setValue happens at postBuild" $ runWD $ do
798+ textarea <- findElemWithRetry $ WD.ByTag "textarea"
799+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pb"
800+ p <- findElemWithRetry (WD.ByTag "p")
801+ shouldContainText "pb" p
802+ testWidget checkBoth checkBoth $ do
804+ e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb)
805+ el "p" $ dynText $ _textAreaElement_value e
806+ it "does not fail when both setValue AND user updated value happen before switchover" $ runWD $ do
807+ let checkStatic = do
808+ textarea <- findElemWithRetry $ WD.ByTag "textarea"
809+ h2_value <- findElemWithRetry (WD.ByTag "h2")
810+ h3_input <- findElemWithRetry (WD.ByTag "h3")
811+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pb"
812+ shouldContainText "pb" h2_value
813+ shouldContainText "" h3_input
814+ WD.sendKeys "abc" textarea
815+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc"
816+ shouldContainText "pb" h2_value
817+ shouldContainText "" h3_input
818+ pure (textarea, h2_value, h3_input)
819+ checkHydrated (textarea, h2_value, h3_input) = do
820+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc"
821+ shouldContainText "pbabc" h3_input
822+ shouldContainText "pbabc pb" h2_value
824+ testWidget' checkStatic checkHydrated $ do
826+ e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb)
827+ el "h1" $ dynText $ _textAreaElement_value e
828+ el "h2" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ updated $ _textAreaElement_value e
829+ el "h3" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ _textAreaElement_input e
830+ it "value is correct when both setValue AND user updated value happen before switchover" $ runWD $ do
831+ let checkStatic = do
832+ textarea <- findElemWithRetry $ WD.ByTag "textarea"
833+ p <- findElemWithRetry (WD.ByTag "p")
834+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pb"
835+ shouldContainText "pb" p
836+ WD.sendKeys "abc" textarea
837+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc"
838+ shouldContainText "pb" p -- It won't be updated yet
840+ checkHydrated (textarea, p) = do
841+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc"
842+ shouldContainText "pbabc" p
844+ testWidget' checkStatic checkHydrated $ do
846+ e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb)
847+ el "p" $ dynText $ _textAreaElement_value e
536− describe "hydration/immediate" $ session' $ do
849+ describe "hydration/immediate" $ do
537 it "captures user input after switchover" $ runWD $ do
850 it "captures user input after switchover" $ runWD $ do
538 inputRef :: IORef Text <- newRef ""
851 inputRef :: IORef Text <- newRef ""
539 let checkValue = do
852 let checkValue = do
@@ -575,13 +888,13 @@ tests withDebugging wdConfig caps _selenium = do
575 performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
888 performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
576 performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
889 performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
578− describe "selectElement" $ do
891+ session' "selectElement" $ do
579 let options :: DomBuilder t m => m ()
892 let options :: DomBuilder t m => m ()
580 options = do
893 options = do
581 elAttr "option" ("value" =: "one" <> "id" =: "one") $ text "one"
894 elAttr "option" ("value" =: "one" <> "id" =: "one") $ text "one"
582 elAttr "option" ("value" =: "two" <> "id" =: "two") $ text "two"
895 elAttr "option" ("value" =: "two" <> "id" =: "two") $ text "two"
583 elAttr "option" ("value" =: "three" <> "id" =: "three") $ text "three"
896 elAttr "option" ("value" =: "three" <> "id" =: "three") $ text "three"
584− describe "hydration" $ session' $ do
897+ describe "hydration" $ do
585 it "sets initial value correctly" $ runWD $ do
898 it "sets initial value correctly" $ runWD $ do
586 inputRef <- newRef ("" :: Text)
899 inputRef <- newRef ("" :: Text)
587 let setup = do
900 let setup = do
@@ -655,7 +968,7 @@ tests withDebugging wdConfig caps _selenium = do
655 performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e
968 performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e
656 performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e)
969 performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e)
658− describe "hydration/immediate" $ session' $ do
971+ describe "hydration/immediate" $ do
659 it "captures user input after switchover" $ runWD $ do
972 it "captures user input after switchover" $ runWD $ do
660 inputRef :: IORef Text <- newRef ""
973 inputRef :: IORef Text <- newRef ""
661 let checkValue = do
974 let checkValue = do
@@ -701,7 +1014,7 @@ tests withDebugging wdConfig caps _selenium = do
701 performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e
1014 performEvent_ $ liftIO . writeRef valueByUIRef <$> _selectElement_change e
702 performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e)
1015 performEvent_ $ liftIO . writeRef valueRef <$> updated (_selectElement_value e)
704− describe "prerender" $ session' $ do
1017+ session' "prerender" $ do
705 it "works in simple case" $ runWD $ do
1018 it "works in simple case" $ runWD $ do
706 testWidget (checkBodyText "One") (checkBodyText "Two") $ do
1019 testWidget (checkBodyText "One") (checkBodyText "Two") $ do
707 prerender_ (text "One") (text "Two")
1020 prerender_ (text "One") (text "Two")
@@ -770,7 +1083,16 @@ tests withDebugging wdConfig caps _selenium = do
770 prerender_ (pure ()) (liftIO $ trigger "Client")
1083 prerender_ (pure ()) (liftIO $ trigger "Client")
771 textNode $ TextNodeConfig "Initial" $ Just e
1084 textNode $ TextNodeConfig "Initial" $ Just e
1086+ session' "namespaces" $ do
1087+ it "dyn can be nested in namespaced widget" $ runWD $ do
1088+ testWidget (pure ()) (checkTextInTag "svg" "one") $ do
1089+ let svgRootCfg = def
1090+ & (elementConfig_namespace ?~ "http://www.w3.org/2000/svg")
1091+ & (elementConfig_initialAttributes .~ ("width" =: "100%" <> "height" =: "100%" <> "viewBox" =: "0 0 1536 2048"))
1092+ void $ element "svg" svgRootCfg $ do
1093+ dyn_ $ text "one" <$ pure ()
773− describe "runWithReplace" $ session' $ do
1095+ session' "runWithReplace" $ do
774 it "works" $ runWD $ do
1096 it "works" $ runWD $ do
775 replaceChan :: Chan Text <- liftIO newChan
1097 replaceChan :: Chan Text <- liftIO newChan
776 let setup = findElemWithRetry $ WD.ByTag "div"
1098 let setup = findElemWithRetry $ WD.ByTag "div"
@@ -914,7 +1236,61 @@ tests withDebugging wdConfig caps _selenium = do
914 , el "span" . text <$> replace
1236 , el "span" . text <$> replace
1239+ let checkInnerHtml t x = findElemWithRetry (WD.ByTag t) >>= (`attr` "innerHTML") >>= (`shouldBe` Just x)
1240+ it "removes bracketing comments" $ runWD $ do
1241+ replaceChan :: Chan () <- liftIO newChan
1243+ preSwitchover = checkInnerHtml "div" "before|<!--replace-start-0-->inner1<!--replace-end-0-->|after"
1245+ liftIO $ writeChan replaceChan () -- trigger creation of p tag
1246+ _ <- findElemWithRetry $ WD.ByTag "p" -- wait till p tag is created
1247+ checkInnerHtml "div" "before|<p>inner2</p>|after"
1248+ testWidget' preSwitchover check $ do
1249+ replace <- triggerEventWithChan replaceChan
1252+ _ <- runWithReplace (text "inner1") $ el "p" (text "inner2") <$ replace
1254+ it "ignores extra ending bracketing comment" $ runWD $ do
1255+ replaceChan :: Chan () <- liftIO newChan
1257+ preSwitchover = checkInnerHtml "div" "before|<!--replace-start-0-->inner1<!--replace-end-0--><!--replace-end-0-->|after"
1259+ liftIO $ writeChan replaceChan () -- trigger creation of p tag
1260+ _ <- findElemWithRetry $ WD.ByTag "p" -- wait till p tag is created
1261+ checkInnerHtml "div" "before|inner2|after"
1262+ testWidget' preSwitchover check $ do
1263+ replace <- triggerEventWithChan replaceChan
1266+ _ <- runWithReplace (text "inner1" *> comment "replace-end-0") $ text "inner2" <$ replace
1268+ void $ runWithReplace blank $ el "p" blank <$ replace -- Signal tag for end of test
1269+ -- TODO This test actually causes a hydration failure, but it wasn't
1270+ -- previously detected, so I've marked it pending
1271+ xit "ignores missing ending bracketing comments" $ runWD $ do
1272+ replaceChan :: Chan () <- liftIO newChan
1275+ checkInnerHtml "div" "before|<!--replace-start-0-->inner1<!--replace-end-0-->|after"
1276+ divEl <- findElemWithRetry (WD.ByTag "div")
1277+ let wrongHtml = "<!--replace-start-0-->inner1"
1278+ actualHtml :: String <- WD.executeJS
1279+ [WD.JSArg divEl, WD.JSArg wrongHtml]
1280+ "arguments[0].innerHTML = arguments[1]; return arguments[0].innerHTML"
1281+ actualHtml `shouldBe` wrongHtml
1283+ liftIO $ writeChan replaceChan () -- trigger creation of p tag
1284+ _ <- findElemWithRetry $ WD.ByTag "p" -- wait till p tag is created
1285+ checkInnerHtml "div" "before|<p>inner2</p>|after"
1286+ testWidget' preSwitchover check $ do
1287+ replace <- triggerEventWithChan replaceChan
1290+ _ <- runWithReplace (text "inner1") $ el "p" (text "inner2") <$ replace
917− describe "traverseDMapWithKeyWithAdjust" $ session' $ do
1293+ session' "traverseDMapWithKeyWithAdjust" $ do
918 let widget :: DomBuilder t m => DKey a -> Identity a -> m (Identity a)
1294 let widget :: DomBuilder t m => DKey a -> Identity a -> m (Identity a)
919 widget k (Identity v) = elAttr "li" ("id" =: textKey k) $ do
1295 widget k (Identity v) = elAttr "li" ("id" =: textKey k) $ do
920 elClass "span" "key" $ text $ textKey k
1296 elClass "span" "key" $ text $ textKey k
@@ -1017,7 +1393,7 @@ tests withDebugging wdConfig caps _selenium = do
1017 (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap $ leftmost [postBuildPatch <$ pb, replace]
1393 (dmap, _evt) <- traverseDMapWithKeyWithAdjust widget keyMap $ leftmost [postBuildPatch <$ pb, replace]
1018 liftIO $ dmap `H.shouldBe` keyMap
1394 liftIO $ dmap `H.shouldBe` keyMap
1020− describe "traverseIntMapWithKeyWithAdjust" $ session' $ do
1396+ session' "traverseIntMapWithKeyWithAdjust" $ do
1021 let textKeyInt k = "key" <> T.pack (show k)
1397 let textKeyInt k = "key" <> T.pack (show k)
1022 intMap = IntMap.fromList
1398 intMap = IntMap.fromList
1023 [ (1, "one")
1399 [ (1, "one")
@@ -1124,7 +1500,7 @@ tests withDebugging wdConfig caps _selenium = do
1124 (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap $ leftmost [postBuildPatch <$ pb, replace]
1500 (dmap, _evt) <- traverseIntMapWithKeyWithAdjust widget intMap $ leftmost [postBuildPatch <$ pb, replace]
1125 liftIO $ dmap `H.shouldBe` intMap
1501 liftIO $ dmap `H.shouldBe` intMap
1127− describe "traverseDMapWithKeyWithAdjustWithMove" $ session' $ do
1503+ session' "traverseDMapWithKeyWithAdjustWithMove" $ do
1128 let widget :: DomBuilder t m => Key2 a -> Identity a -> m (Identity a)
1504 let widget :: DomBuilder t m => Key2 a -> Identity a -> m (Identity a)
1129 widget k (Identity v) = elAttr "li" ("id" =: textKey2 k) $ do
1505 widget k (Identity v) = elAttr "li" ("id" =: textKey2 k) $ do
1130 elClass "span" "key" $ text $ textKey2 k
1506 elClass "span" "key" $ text $ textKey2 k
@@ -1204,8 +1580,8 @@ tests withDebugging wdConfig caps _selenium = do
1204 (dmap, _evt) <- traverseDMapWithKeyWithAdjustWithMove widget initMap =<< triggerEventWithChan chan
1580 (dmap, _evt) <- traverseDMapWithKeyWithAdjustWithMove widget initMap =<< triggerEventWithChan chan
1205 liftIO $ assertEqual "DMap" initMap dmap
1581 liftIO $ assertEqual "DMap" initMap dmap
1207− describe "hydrating invalid HTML" $ session' $ do
1583+ session' "hydrating invalid HTML" $ do
1208− it "can hydrate list in paragraph" $ runWD $ do
1584+ xit "can hydrate list in paragraph" $ runWD $ do
1209 let preSwitchover = do
1585 let preSwitchover = do
1210 checkBodyText "before\ninner\nafter"
1586 checkBodyText "before\ninner\nafter"
1211 -- Two <p> tags should be present
1587 -- Two <p> tags should be present
@@ -1222,7 +1598,8 @@ tests withDebugging wdConfig caps _selenium = do
1222 shouldContainText "before\ninner\nafter" p1
1598 shouldContainText "before\ninner\nafter" p1
1223 elementShouldBeRemoved ol
1599 elementShouldBeRemoved ol
1224 elementShouldBeRemoved p2
1600 elementShouldBeRemoved p2
1601+ -- Don't fail fatally when hydration encounters the invalid DOM
1225− testWidget' preSwitchover check $ do
1602+ testWidgetDebug' False withDebugging preSwitchover check $ do
1226 -- This is deliberately invalid HTML, the browser will interpret it as
1603 -- This is deliberately invalid HTML, the browser will interpret it as
1227 -- <p>before</p><ol>inner</ol>after<p></p>
1604 -- <p>before</p><ol>inner</ol>after<p></p>
1228 el "p" $ do
1605 el "p" $ do
@@ -1230,6 +1607,29 @@ tests withDebugging wdConfig caps _selenium = do
1230 el "ol" $ text "inner"
1607 el "ol" $ text "inner"
1231 text "after"
1608 text "after"
1610+ -- TODO: This test presupposes the exact set of labels that "dropdown" places in the "value" fields to distinguish options.
1611+ -- This dependence on internal implementation details is undesirable in a test case, but seems fairly tricky to avoid.
1612+ -- It seems expedient for the time being to expect this test case to be updated, should those implementation details ever change.
1613+ session' "dropdown" $ do
1614+ let doTest expectedOpts (initialValue :: Text) = do
1616+ es <- findElemsWithRetry $ WD.ByTag "option"
1617+ opts <- mapM fetchElement es
1618+ assertEqual "missing/extra/incorrect option element(s)" expectedOpts (sort opts)
1619+ testWidget doCheck doCheck $ do
1620+ void $ dropdown initialValue (constDyn (("aa" :: Text) =: "aaa" <> "bb" =: "bbb")) def
1621+ fetchElement e = do
1622+ val <- WD.attr e "value"
1623+ sel <- WD.attr e "selected"
1624+ return (fromMaybe "" val, isJust sel)
1625+ -- 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)
1626+ -- Thus, it would appear that we do actually need a HTML5 or maybe XML parser for this test suite.
1627+ xit "statically renders initial values (on aa)" $ runWD $ do
1628+ doTest [("0",True),("1",False)] "aa"
1629+ -- 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
1630+ xit "statically renders initial values (on bb)" $ runWD $ do
1631+ doTest [("0",False),("1",True)] "bb"
1233data Selenium = Selenium
1633data Selenium = Selenium
1234 { _selenium_portNumber :: PortNumber
1634 { _selenium_portNumber :: PortNumber
1235 , _selenium_stopServer :: IO ()
1635 , _selenium_stopServer :: IO ()
@@ -1297,6 +1697,9 @@ checkTextInId i expected = do
1297findElemWithRetry :: Selector -> WD WD.Element
1697findElemWithRetry :: Selector -> WD WD.Element
1298findElemWithRetry = withRetry . WD.findElem
1698findElemWithRetry = withRetry . WD.findElem
1700+findElemsWithRetry :: Selector -> WD [WD.Element]
1701+findElemsWithRetry = withRetry . WD.findElems
1300getBody :: WD WD.Element
1703getBody :: WD WD.Element
1301getBody = WD.findElem $ WD.ByTag "body"
1704getBody = WD.findElem $ WD.ByTag "body"
@@ -1305,8 +1708,8 @@ withRetry a = wait 300
1305 where wait :: Int -> WD a
1708 where wait :: Int -> WD a
1306 wait 0 = a
1709 wait 0 = a
1307 wait n = try a >>= \case
1710 wait n = try a >>= \case
1308− Left (_ :: SomeException) -> do
1711+ Left (e :: SomeException) -> do
1309− liftIO $ threadDelay 100000
1712+ liftIO $ putStrLn ("(retrying due to " <> show e <> ")") *> threadDelay 100000
1310 wait $ n - 1
1713 wait $ n - 1
1311 Right v -> return v
1714 Right v -> return v
@@ -1322,11 +1725,12 @@ testWidgetStaticDebug
1322 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1725 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1323 -- ^ Widget we are testing
1726 -- ^ Widget we are testing
1325−testWidgetStaticDebug withDebugging w = testWidgetDebug withDebugging (void w) w
1728+testWidgetStaticDebug withDebugging w = testWidgetDebug True withDebugging (void w) w
1327-- | TODO: do something about JSExceptions not causing tests to fail
1730-- | TODO: do something about JSExceptions not causing tests to fail
1328testWidgetDebug
1731testWidgetDebug
1330 -> WD ()
1734 -> WD ()
1331 -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
1735 -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
@@ -1334,12 +1738,16 @@ testWidgetDebug
1334 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1738 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1335 -- ^ Widget we are testing
1739 -- ^ Widget we are testing
1337−testWidgetDebug withDebugging beforeJS afterSwitchover =
1741+testWidgetDebug hardFailure withDebugging beforeJS afterSwitchover =
1338− testWidgetDebug' withDebugging beforeJS (const afterSwitchover)
1742+ testWidgetDebug' hardFailure withDebugging beforeJS (const afterSwitchover)
1744+data HydrationFailedException = HydrationFailedException deriving Show
1745+instance Exception HydrationFailedException
1340-- | TODO: do something about JSExceptions not causing tests to fail
1747-- | TODO: do something about JSExceptions not causing tests to fail
1341testWidgetDebug'
1748testWidgetDebug'
1344 -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
1752 -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
1345 -> (a -> WD b)
1753 -> (a -> WD b)
@@ -1347,7 +1755,7 @@ testWidgetDebug'
1347 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1755 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
1348 -- ^ Widget we are testing (contents of body)
1756 -- ^ Widget we are testing (contents of body)
1350−testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
1758+testWidgetDebug' hardFailure withDebugging beforeJS afterSwitchover bodyWidget = do
1351 let putStrLnDebug :: MonadIO m => Text -> m ()
1759 let putStrLnDebug :: MonadIO m => Text -> m ()
1352 putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
1760 putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
1353 staticApp = do
1761 staticApp = do
@@ -1359,6 +1767,7 @@ testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
1359 ((), html) <- liftIO $ renderStatic $ runHydratableT staticApp
1767 ((), html) <- liftIO $ renderStatic $ runHydratableT staticApp
1360 putStrLnDebug "rendered static"
1768 putStrLnDebug "rendered static"
1361 waitBeforeJS <- liftIO newEmptyMVar -- Empty until JS should be run
1769 waitBeforeJS <- liftIO newEmptyMVar -- Empty until JS should be run
1770+ onFailure <- if hardFailure then (`throwTo` HydrationFailedException) <$> liftIO myThreadId else pure $ pure ()
1362 waitUntilSwitchover <- liftIO newEmptyMVar -- Empty until switchover
1771 waitUntilSwitchover <- liftIO newEmptyMVar -- Empty until switchover
1363 let entryPoint = do
1772 let entryPoint = do
1364 putStrLnDebug "taking waitBeforeJS"
1773 putStrLnDebug "taking waitBeforeJS"
@@ -1370,7 +1779,7 @@ testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
1370 liftIO $ putMVar waitUntilSwitchover ()
1779 liftIO $ putMVar waitUntilSwitchover ()
1371 putStrLnDebug "put waitUntilSwitchover"
1780 putStrLnDebug "put waitUntilSwitchover"
1372 putStrLnDebug "running mainHydrationWidgetWithSwitchoverAction"
1781 putStrLnDebug "running mainHydrationWidgetWithSwitchoverAction"
1373− mainHydrationWidgetWithSwitchoverAction switchOverAction blank bodyWidget
1782+ mainHydrationWidgetWithSwitchoverActionWithFailure' onFailure switchOverAction blank bodyWidget
1374 putStrLnDebug "syncPoint after mainHydrationWidgetWithSwitchoverAction"
1783 putStrLnDebug "syncPoint after mainHydrationWidgetWithSwitchoverAction"
1375 syncPoint
1784 syncPoint
1376 application <- liftIO $ jsaddleOr defaultConnectionOptions entryPoint $ \_ sendResponse -> do
1785 application <- liftIO $ jsaddleOr defaultConnectionOptions entryPoint $ \_ sendResponse -> do
@@ -1388,23 +1797,26 @@ testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
1389 -- hSilence to get rid of ConnectionClosed logs
1798 -- hSilence to get rid of ConnectionClosed logs
1390 silenceIfDebug = if withDebugging then id else hSilence [stderr]
1799 silenceIfDebug = if withDebugging then id else hSilence [stderr]
1391− jsaddleWarp = forkIO $ silenceIfDebug $ Warp.runSettings settings application
1800+ jsaddleWarp = silenceIfDebug $ Warp.runSettings settings application
1392− jsaddleTid <- liftIO jsaddleWarp
1801+ withAsync' jsaddleWarp $ do
1393− putStrLnDebug "taking waitJSaddle"
1802+ putStrLnDebug "taking waitJSaddle"
1394− liftIO $ takeMVar waitJSaddle
1803+ liftIO $ takeMVar waitJSaddle
1395− putStrLnDebug "opening page"
1804+ putStrLnDebug "opening page"
1396− WD.openPage $ "http://localhost:" <> show jsaddlePort
1805+ WD.openPage $ "http://localhost:" <> show jsaddlePort
1397− putStrLnDebug "running beforeJS"
1806+ putStrLnDebug "running beforeJS"
1398− a <- beforeJS
1807+ a <- beforeJS
1399− putStrLnDebug "putting waitBeforeJS"
1808+ putStrLnDebug "putting waitBeforeJS"
1400− liftIO $ putMVar waitBeforeJS ()
1809+ liftIO $ putMVar waitBeforeJS ()
1401− putStrLnDebug "taking waitUntilSwitchover"
1810+ putStrLnDebug "taking waitUntilSwitchover"
1402− liftIO $ takeMVar waitUntilSwitchover
1811+ liftIO $ takeMVar waitUntilSwitchover
1403− putStrLnDebug "running afterSwitchover"
1812+ putStrLnDebug "running afterSwitchover"
1404− b <- afterSwitchover a
1813+ afterSwitchover a
1405− putStrLnDebug "killing jsaddle thread"
1814+
1406− liftIO $ killThread jsaddleTid
1815+withAsync' :: (MonadIO m, MonadMask m) => IO a -> m b -> m b
1407− return b
1816+withAsync' f g = bracket
1817+ (liftIO $ Async.async f)
1818+ (liftIO . Async.uninterruptibleCancel)
1409data Key2 a where
1821data Key2 a where
1410 Key2_Int :: Int -> Key2 Int
1822 Key2_Int :: Int -> Key2 Int