on develop help sign in
More fixes for input/textArea elements, improve test suite

- Fix some bad behaviour around what happens at switchover with input type elements - Allow the test suite to report hydration failures. This also exposed an unrelated test failure which we previously missed. I marked it as pending for now - when the test suite was originally written, the hydration failures were always obvious in the logs during development, so it was difficult to add new failing tests. At some point we ended up being much more verbose by default, so the failure message got hidden.

commit8d034f2dfbe0a210a7d82b629a2c4f8dadcbb51d authorTom Smalley <tfsmalley@gmail.com> dateFri 10 Jul 2020, 20:59 (6 years ago) parent0b93790
4 files changed+330−62
reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs+35−31 reflex-dom-core/src/Reflex/Dom/Builder/Static.hs+10−8 reflex-dom-core/src/Reflex/Dom/Main.hs+13−11 reflex-dom-core/test/hydration.hs+272−12
reflex-dom-core/src/Reflex/Dom/Builder/Immediate.hs+35−31
@@ -292,11 +292,12 @@ localRunner (HydrationRunnerT m) s parent = do
292292{-# INLINABLE runHydrationRunnerT #-}
293293runHydrationRunnerT
294294 :: (MonadRef m, Ref m ~ IORef, Monad m, PerformEvent t m, MonadFix m, MonadReflexCreateTrigger t m, MonadJSM m, MonadJSM (Performable m))
295− => HydrationRunnerT t m a -> Maybe Node -> Node -> Chan [DSum (EventTriggerRef t) TriggerInvocation] -> m a
296−runHydrationRunnerT (HydrationRunnerT m) s parent events = flip runDomRenderHookT events $ flip runReaderT parent $ do
295+ => HydrationRunnerT t m a -> IO () -> Maybe Node -> Node -> Chan [DSum (EventTriggerRef t) TriggerInvocation] -> m a
296+runHydrationRunnerT (HydrationRunnerT m) onFailure s parent events = flip runDomRenderHookT events $ flip runReaderT parent $ do
297297 (a, s') <- runStateT m (HydrationState s False)
298298 traverse_ removeSubsequentNodes $ _hydrationState_previousNode s'
299299 when (_hydrationState_failed s') $ liftIO $ putStrLn "reflex-dom warning: hydration failed: the DOM was not as expected at switchover time. This may be due to invalid HTML which the browser has altered upon parsing, some external JS altering the DOM, or the page being served from an outdated cache."
300+ when (_hydrationState_failed s') $ liftIO onFailure
300301 pure a
301302
302303instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (HydrationRunnerT t m) where
@@ -938,17 +939,15 @@ inputElementInternal cfg = getHydrationMode >>= \case
938939 domElement <- liftIO $ readIORef domElementRef
939940 let domInputElement = uncheckedCastTo DOM.HTMLInputElement domElement
940941 getValue = Input.getValue domInputElement
941− -- Fire the appropriate events if the value has changed somehow
942− liftJSM getValue >>= \realValue -> liftIO $ if
943− -- The user could have altered the value before switchover
944− | realValue /= switchoverValue -> triggerChangeByUI realValue
945− -- When the value has been updated by setValue before switchover, but
946− -- the user hasn't entered text, we must send an update here to remain
947− -- in sync. This is because the later requestDomAction based on the
948− -- setValue event will not capture events happening before postBuild,
949− -- because this code runs after switchover.
950− | v0 /= switchoverValue -> triggerChangeBySetValue switchoverValue
951− | otherwise -> pure ()
942+ -- When the value has been updated by setValue before switchover, we must
943+ -- send an update here to remain in sync. This is because the later
944+ -- requestDomAction based on the setValue event will not capture events
945+ -- happening before postBuild, because this code runs after switchover.
946+ when (v0 /= switchoverValue) $ liftIO $ triggerChangeBySetValue switchoverValue
947+ -- The user could have altered the value before switchover. This must be
948+ -- triggered after the setValue one in order for the events to be in the
949+ -- correct order.
950+ liftJSM getValue >>= \realValue -> when (realValue /= switchoverValue) $ liftIO $ triggerChangeByUI realValue
952951 -- Watch for user interaction and trigger event accordingly
953952 requestDomAction_ $ (liftJSM getValue >>= liftIO . triggerChangeByUI) <$ Reflex.select (_element_events e) (WrapArg Input)
954953 for_ (_inputElementConfig_setValue cfg) $ \eSetValue ->
@@ -962,19 +961,17 @@ inputElementInternal cfg = getHydrationMode >>= \case
962961 ]
963962 liftIO . triggerFocusChange =<< Node.isSameNode (toNode domElement) . fmap toNode =<< Document.getActiveElement doc
964963 requestDomAction_ $ liftIO . triggerFocusChange <$> focusChange'
965− -- Fire the appropriate events if the checked state has changed somehow
966− liftJSM (Input.getChecked domInputElement) >>= \realChecked -> liftIO $ if
967− -- The user could have clicked the checkbox before switchover, we only
968− -- detect cases where they flipped the state
969− | realChecked /= switchoverChecked -> triggerCheckedChangedByUI realChecked
970− -- When the checked state has been updated by setChecked before
971− -- switchover, but the user hasn't changed the state, we must send an
972− -- update here to remain in sync. This is because the later
973− -- requestDomAction based on the setChecked event will not capture
974− -- events happening before postBuild, because this code runs after
975− -- switchover.
976− | c0 /= switchoverChecked -> triggerCheckedChangedBySetChecked switchoverChecked
977− | otherwise -> pure ()
964+ -- When the checked state has been updated by setChecked before
965+ -- switchover, we must send an update here to remain in sync. This is
966+ -- because the later requestDomAction based on the setChecked event will not
967+ -- capture events happening before postBuild, because this code runs after
968+ -- switchover.
969+ when (c0 /= switchoverChecked) $ liftIO $ triggerCheckedChangedBySetChecked switchoverChecked
970+ -- The user could have clicked the checkbox before switchover, we only
971+ -- detect cases where they flipped the state. This must be triggered after
972+ -- the setValue one in order for the events to be in the correct order.
973+ liftJSM (Input.getChecked domInputElement) >>= \realChecked -> when (realChecked /= switchoverChecked) $
974+ liftIO $ triggerCheckedChangedByUI realChecked
978975 _ <- liftJSM $ domInputElement `on` Events.click $ do
979976 liftIO . triggerCheckedChangedByUI =<< Input.getChecked domInputElement
980977 for_ (_inputElementConfig_setChecked cfg) $ \eNewchecked ->
@@ -1059,14 +1056,21 @@ textAreaElementInternal cfg = getHydrationMode >>= \case
10591056 doc <- askDocument
10601057 -- Expected initial value from config
10611058 let v0 = _textAreaElementConfig_initialValue cfg
1062− addHydrationStep $ do
1059+ valueAtSwitchover = maybe (pure $ pure v0) (hold v0) (_textAreaElementConfig_setValue cfg)
1060+ addHydrationStepWithSetup valueAtSwitchover $ \switchoverValue' -> do
1061+ switchoverValue <- sample switchoverValue'
10631062 domElement <- liftIO $ readIORef domElementRef
10641063 let domTextAreaElement = uncheckedCastTo DOM.HTMLTextAreaElement domElement
10651064 getValue = TextArea.getValue domTextAreaElement
1066− -- The browser might have messed with the value, or the user could have
1067− -- altered it before activation, so we set it if it isn't what we expect
1068− liftJSM getValue >>= \v0' -> do
1069− when (v0' /= v0) $ liftIO $ triggerChangeByUI v0'
1065+ -- When the value has been updated by setValue before switchover, we must
1066+ -- send an update here to remain in sync. This is because the later
1067+ -- requestDomAction based on the setValue event will not capture events
1068+ -- happening before postBuild, because this code runs after switchover.
1069+ when (v0 /= switchoverValue) $ liftIO $ triggerChangeBySetValue switchoverValue
1070+ -- The user could have altered the value before switchover. This must be
1071+ -- triggered after the setValue one in order for the events to be in the
1072+ -- correct order.
1073+ liftJSM getValue >>= \realValue -> when (realValue /= switchoverValue) $ liftIO $ triggerChangeByUI realValue
10701074 -- Watch for user interaction and trigger event accordingly
10711075 requestDomAction_ $ (liftJSM getValue >>= liftIO . triggerChangeByUI) <$ Reflex.select (_element_events e) (WrapArg Input)
10721076 for_ (_textAreaElementConfig_setValue cfg) $ \eSetValue ->
reflex-dom-core/src/Reflex/Dom/Builder/Static.hs+10−8
@@ -328,16 +328,16 @@ instance SupportsStaticDomBuilder t m => DomBuilder t (StaticDomBuilderT t m) wh
328328 & elementConfig_initialAttributes %~ setInitialValue . setInitialChecked
329329 & elementConfig_modifyAttributes %~ setUpdatedValue . setUpdatedChecked
330330 (e, _result) <- element "input" adjustedConfig $ return ()
331− v0 <- case _inputElementConfig_setValue cfg of
331+ v <- case _inputElementConfig_setValue cfg of
332332 Nothing -> pure $ constDyn (cfg ^. inputElementConfig_initialValue)
333− Just e -> holdDyn (cfg ^. inputElementConfig_initialValue) e
334− c0 <- case _inputElementConfig_setChecked cfg of
333+ Just ev -> holdDyn (cfg ^. inputElementConfig_initialValue) ev
334+ c <- case _inputElementConfig_setChecked cfg of
335335 Nothing -> pure $ constDyn $ _inputElementConfig_initialChecked cfg
336− Just e -> holdDyn (_inputElementConfig_initialChecked cfg) e
336+ Just ev -> holdDyn (_inputElementConfig_initialChecked cfg) ev
337337 let hasFocus = constDyn False -- TODO should this be coming from initialAtttributes
338338 return $ InputElement
339− { _inputElement_value = v0
340− , _inputElement_checked = c0
339+ { _inputElement_value = v
340+ , _inputElement_checked = c
341341 , _inputElement_checkedChange = never
342342 , _inputElement_input = never
343343 , _inputElement_hasFocus = hasFocus
@@ -352,10 +352,12 @@ instance SupportsStaticDomBuilder t m => DomBuilder t (StaticDomBuilderT t m) wh
352352 void $ textNode $ def
353353 & textNodeConfig_initialContents .~ _textAreaElementConfig_initialValue cfg
354354 & textNodeConfig_setContents .~ fromMaybe never (_textAreaElementConfig_setValue cfg)
355− let v0 = constDyn $ cfg ^. textAreaElementConfig_initialValue
355+ v <- case _textAreaElementConfig_setValue cfg of
356+ Nothing -> pure $ constDyn (cfg ^. textAreaElementConfig_initialValue)
357+ Just ev -> holdDyn (cfg ^. textAreaElementConfig_initialValue) ev
356358 let hasFocus = constDyn False -- TODO should this be coming from initialAtttributes
357359 return $ TextAreaElement
358− { _textAreaElement_value = v0
360+ { _textAreaElement_value = v
359361 , _textAreaElement_input = never
360362 , _textAreaElement_hasFocus = hasFocus
361363 , _textAreaElement_element = e
reflex-dom-core/src/Reflex/Dom/Main.hs+13−11
@@ -64,23 +64,24 @@ mainHydrationWidgetWithHead = mainHydrationWidgetWithHead'
6464{-# INLINABLE mainHydrationWidgetWithHead' #-}
6565-- | Warning: `mainHydrationWidgetWithHead'` is provided only as performance tweak. It is expected to disappear in future releases.
6666mainHydrationWidgetWithHead' :: HydrationWidget () () -> HydrationWidget () () -> JSM ()
67−mainHydrationWidgetWithHead' = mainHydrationWidgetWithSwitchoverAction' (pure ())
67+mainHydrationWidgetWithHead' = mainHydrationWidgetWithSwitchoverAction' (pure ()) (pure ())
6868
6969{-# INLINE mainHydrationWidgetWithSwitchoverAction #-}
70−mainHydrationWidgetWithSwitchoverAction :: JSM () -> (forall x. HydrationWidget x ()) -> (forall x. HydrationWidget x ()) -> JSM ()
70+mainHydrationWidgetWithSwitchoverAction :: IO () -> JSM () -> (forall x. HydrationWidget x ()) -> (forall x. HydrationWidget x ()) -> JSM ()
7171mainHydrationWidgetWithSwitchoverAction = mainHydrationWidgetWithSwitchoverAction'
7272
7373{-# INLINABLE mainHydrationWidgetWithSwitchoverAction' #-}
7474-- | Warning: `mainHydrationWidgetWithSwitchoverAction'` is provided only as performance tweak. It is expected to disappear in future releases.
75−mainHydrationWidgetWithSwitchoverAction' :: JSM () -> HydrationWidget () () -> HydrationWidget () () -> JSM ()
76−mainHydrationWidgetWithSwitchoverAction' switchoverAction head' body = do
77− runHydrationWidgetWithHeadAndBody switchoverAction $ \appendHead appendBody -> do
75+mainHydrationWidgetWithSwitchoverAction' :: IO () -> JSM () -> HydrationWidget () () -> HydrationWidget () () -> JSM ()
76+mainHydrationWidgetWithSwitchoverAction' onFailure switchoverAction head' body = do
77+ runHydrationWidgetWithHeadAndBody onFailure switchoverAction $ \appendHead appendBody -> do
7878 appendHead head'
7979 appendBody body
8080
8181{-# INLINABLE attachHydrationWidget #-}
8282attachHydrationWidget
83− :: JSM ()
83+ :: IO ()
84+ -> JSM ()
8485 -> JSContextSingleton ()
8586 -> ( Event DomTimeline ()
8687 -> IORef HydrationMode
@@ -89,7 +90,7 @@ attachHydrationWidget
8990 -> PerformEventT DomTimeline DomHost (a, IORef (Maybe (EventTrigger DomTimeline ())))
9091 )
9192 -> IO (a, FireCommand DomTimeline DomHost)
92−attachHydrationWidget switchoverAction jsSing w = do
93+attachHydrationWidget onFailure switchoverAction jsSing w = do
9394 hydrationMode <- liftIO $ newIORef HydrationMode_Hydrating
9495 rootNodesRef <- liftIO $ newIORef []
9596 events <- newChan
@@ -105,7 +106,7 @@ attachHydrationWidget switchoverAction jsSing w = do
105106 rootNodes <- liftIO $ readIORef rootNodesRef
106107 let delayedAction = do
107108 for_ (reverse rootNodes) $ \(rootNode, runner) -> do
108− let hydrate = runHydrationRunnerT runner Nothing rootNode events
109+ let hydrate = runHydrationRunnerT runner onFailure Nothing rootNode events
109110 void $ runWithJSContextSingleton (runPostBuildT hydrate never) jsSing
110111 liftIO $ writeIORef hydrationMode HydrationMode_Immediate
111112 runWithJSContextSingleton (DOM.liftJSM switchoverAction) jsSing
@@ -120,17 +121,18 @@ type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (Perform
120121
121122{-# INLINABLE runHydrationWidgetWithHeadAndBody #-}
122123runHydrationWidgetWithHeadAndBody
123− :: JSM ()
124+ :: IO ()
125+ -> JSM ()
124126 -> ( (forall c. HydrationWidget () c -> FloatingWidget () c) -- "Append to head" --TODO: test invoking this more than once
125127 -> (forall c. HydrationWidget () c -> FloatingWidget () c) -- "Append to body" --TODO: test invoking this more than once
126128 -> FloatingWidget () ()
127129 )
128130 -> JSM ()
129−runHydrationWidgetWithHeadAndBody switchoverAction app = withJSContextSingletonMono $ \jsSing -> do
131+runHydrationWidgetWithHeadAndBody onFailure switchoverAction app = withJSContextSingletonMono $ \jsSing -> do
130132 globalDoc <- currentDocumentUnchecked
131133 headElement <- getHeadUnchecked globalDoc
132134 bodyElement <- getBodyUnchecked globalDoc
133− (events, fc) <- liftIO . attachHydrationWidget switchoverAction jsSing $ \switchover hydrationMode hydrationResult events -> do
135+ (events, fc) <- liftIO . attachHydrationWidget onFailure switchoverAction jsSing $ \switchover hydrationMode hydrationResult events -> do
134136 (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
135137 let hydrateDom :: DOM.Node -> HydrationWidget () c -> FloatingWidget () c
136138 hydrateDom n w = do
reflex-dom-core/test/hydration.hs+272−12
@@ -161,9 +161,9 @@ tests withDebugging wdConfig caps _selenium = do
161161 testWidgetStatic :: WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
162162 testWidgetStatic = testWidgetStaticDebug withDebugging
163163 testWidget :: WD () -> WD b -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
164− testWidget = testWidgetDebug withDebugging
164+ testWidget = testWidgetDebug True withDebugging
165165 testWidget' :: WD a -> (a -> WD b) -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ()) -> WD b
166− testWidget' = testWidgetDebug' withDebugging
166+ testWidget' = testWidgetDebug' True withDebugging
167167 describe "text" $ session' $ do
168168 it "works" $ runWD $ do
169169 testWidgetStatic (checkBodyText "hello world") $ do
@@ -313,6 +313,40 @@ tests withDebugging wdConfig caps _selenium = do
313313 & inputElementConfig_initialValue .~ "test"
314314 & inputElementConfig_setValue .~ ("test-updated" <$ pb)
315315 pure ()
316+ it "sets checked attr appropriately" $ runWD $ do
317+ setCheckedChan <- liftIO newChan
318+ let checkStatic = do
319+ e <- findElemWithRetry $ WD.ByTag "input"
320+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
321+ pure e
322+ checkValue e = do
323+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
324+ WD.moveToCenter e
325+ WD.click e -- Click to uncheck
326+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
327+ liftIO $ writeChan setCheckedChan True -- Programatically check the checkbox
328+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
329+ testWidget' checkStatic checkValue $ do
330+ setChecked <- triggerEventWithChan setCheckedChan
331+ _ <- inputElement $ def
332+ & initialAttributes .~ "type" =: "checkbox"
333+ & inputElementConfig_initialChecked .~ True
334+ & inputElementConfig_setChecked .~ setChecked
335+ pure ()
336+ it "sets checked attr appropriately at postbuild" $ runWD $ do
337+ let checkStatic = do
338+ e <- findElemWithRetry $ WD.ByTag "input"
339+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
340+ pure e
341+ checkValue e = do
342+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
343+ testWidget' checkStatic checkValue $ do
344+ pb <- getPostBuild
345+ _ <- inputElement $ def
346+ & initialAttributes .~ "type" =: "checkbox"
347+ & inputElementConfig_initialChecked .~ False
348+ & inputElementConfig_setChecked .~ (True <$ pb)
349+ pure ()
316350 describe "hydration" $ session' $ do
317351 it "doesn't wipe user input when switching over" $ runWD $ do
318352 inputRef <- newRef ("hello " :: Text)
@@ -383,24 +417,49 @@ tests withDebugging wdConfig caps _selenium = do
383417 checkedByUIRef <- newRef False
384418 checkedRef <- newRef False
385419 setCheckedChan <- liftIO newChan
386− let checkValue = do
420+ let checkStatic = do
421+ e <- findElemWithRetry $ WD.ByTag "input"
422+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
423+ pure e
424+ checkValue e = do
387425 readRef checkedByUIRef `shouldBeWithRetryM` False
388426 readRef checkedRef `shouldBeWithRetryM` False
389− e <- findElemWithRetry $ WD.ByTag "input"
427+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
390428 WD.moveToCenter e
391429 WD.click e
392430 readRef checkedByUIRef `shouldBeWithRetryM` True
393431 readRef checkedRef `shouldBeWithRetryM` True
432+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
394433 liftIO $ writeChan setCheckedChan False
395434 readRef checkedByUIRef `shouldBeWithRetryM` True
396435 readRef checkedRef `shouldBeWithRetryM` False
397− testWidget (pure ()) checkValue $ do
436+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
437+ testWidget' checkStatic checkValue $ do
398438 setChecked <- triggerEventWithChan setCheckedChan
399439 e <- inputElement $ def
400440 & initialAttributes .~ "type" =: "checkbox"
401441 & inputElementConfig_setChecked .~ setChecked
402442 performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
403443 performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
444+ it "respects user updates to checked which happen before hydration" $ runWD $ do
445+ checkedByUIRef <- newRef False
446+ checkedRef <- newRef False
447+ let checkStatic = do
448+ e <- findElemWithRetry $ WD.ByTag "input"
449+ WD.attr e "checked" `shouldBeWithRetryM` Nothing
450+ WD.moveToCenter e
451+ WD.click e
452+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
453+ pure e
454+ checkValue e = do
455+ WD.attr e "checked" `shouldBeWithRetryM` Just "true"
456+ readRef checkedByUIRef `shouldBeWithRetryM` True
457+ readRef checkedRef `shouldBeWithRetryM` True
458+ testWidget' checkStatic checkValue $ do
459+ e <- inputElement $ def
460+ & initialAttributes .~ "type" =: "checkbox"
461+ performEvent_ $ liftIO . writeRef checkedByUIRef <$> _inputElement_checkedChange e
462+ performEvent_ $ liftIO . writeRef checkedRef <$> updated (_inputElement_checked e)
404463 it "captures file uploads" $ runWD $ do
405464 filesRef :: IORef [Text] <- newRef []
406465 let uploadFile = do
@@ -416,6 +475,116 @@ tests withDebugging wdConfig caps _selenium = do
416475 prerender_ (pure ()) $ performEvent_ $ ffor (tag (current (_inputElement_files e)) click) $ \fs -> do
417476 names <- liftJSM $ traverse File.getName fs
418477 liftIO $ writeRef filesRef names
478+ it "fires _input event if the user altered the value before hydration" $ runWD $ do
479+ input <- newRef ("" :: Text)
480+ update <- newRef ("" :: Text)
481+ let checkStatic = do
482+ e <- findElemWithRetry $ WD.ByTag "input"
483+ WD.attr e "value" `shouldBeWithRetryM` Just ""
484+ WD.sendKeys "test" e
485+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
486+ pure e
487+ checkHydrated e = do
488+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
489+ readRef input `shouldBeWithRetryM` "test"
490+ readRef update `shouldBeWithRetryM` "test"
491+ testWidget' checkStatic checkHydrated $ do
492+ e <- inputElement def
493+ performEvent_ $ liftIO . writeRef input <$> _inputElement_input e
494+ performEvent_ $ liftIO . writeRef update <$> updated (_inputElement_value e)
495+ it "does not fire _input event when the value is updated at postBuild" $ runWD $ do
496+ input <- newRef (Nothing :: Maybe Text)
497+ let checkStatic = do
498+ e <- findElemWithRetry $ WD.ByTag "input"
499+ WD.attr e "value" `shouldBeWithRetryM` Just "pb"
500+ pure e
501+ checkHydrated e = do
502+ WD.attr e "value" `shouldBeWithRetryM` Just "pb"
503+ readRef input `shouldBeWithRetryM` Nothing
504+ testWidget' checkStatic checkHydrated $ do
505+ pb <- getPostBuild
506+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
507+ performEvent_ $ liftIO . writeRef input . Just <$> _inputElement_input e
508+ it "SSR produces correct DOM based on inputElement values when setValue happens at postBuild" $ runWD $ do
509+ let checkBoth = do
510+ input <- findElemWithRetry $ WD.ByTag "input"
511+ WD.attr input "value" `shouldBeWithRetryM` Just "pb"
512+ p <- findElemWithRetry (WD.ByTag "p")
513+ shouldContainText "pb" p
514+ testWidget checkBoth checkBoth $ do
515+ pb <- getPostBuild
516+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
517+ el "p" $ dynText $ _inputElement_value e
518+ it "does not fail when both setValue AND user updated value happen before switchover" $ runWD $ do
519+ let checkStatic = do
520+ input <- findElemWithRetry $ WD.ByTag "input"
521+ h2_value <- findElemWithRetry (WD.ByTag "h2")
522+ h3_input <- findElemWithRetry (WD.ByTag "h3")
523+ WD.attr input "value" `shouldBeWithRetryM` Just "pb"
524+ shouldContainText "pb" h2_value
525+ shouldContainText "" h3_input
526+ WD.sendKeys "abc" input
527+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
528+ shouldContainText "pb" h2_value
529+ shouldContainText "" h3_input
530+ pure (input, h2_value, h3_input)
531+ checkHydrated (input, h2_value, h3_input) = do
532+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
533+ shouldContainText "pbabc" h3_input
534+ shouldContainText "pbabc pb" h2_value
535+ pure ()
536+ testWidget' checkStatic checkHydrated $ do
537+ pb <- getPostBuild
538+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
539+ el "h1" $ dynText $ _inputElement_value e
540+ el "h2" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ updated $ _inputElement_value e
541+ el "h3" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ _inputElement_input e
542+ it "value is correct when both setValue AND user updated value happen before switchover" $ runWD $ do
543+ let checkStatic = do
544+ input <- findElemWithRetry $ WD.ByTag "input"
545+ p <- findElemWithRetry (WD.ByTag "p")
546+ WD.attr input "value" `shouldBeWithRetryM` Just "pb"
547+ shouldContainText "pb" p
548+ WD.sendKeys "abc" input
549+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
550+ shouldContainText "pb" p -- It won't be updated yet
551+ pure (input, p)
552+ checkHydrated (input, p) = do
553+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
554+ shouldContainText "pbabc" p
555+ pure ()
556+ testWidget' checkStatic checkHydrated $ do
557+ pb <- getPostBuild
558+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
559+ el "p" $ dynText $ _inputElement_value e
560+ it "input event and (updated value) fire correctly when both setValue AND user updated value happen before switchover" $ runWD $ do
561+ valRef <- newRef ([] :: [Text])
562+ inputRef <- newRef ([] :: [Text])
563+ let consRef ref a = liftIO $ atomicModifyRef ref $ \as -> (a:as, ())
564+ checkStatic = do
565+ input <- findElemWithRetry $ WD.ByTag "input"
566+ p <- findElemWithRetry (WD.ByTag "p")
567+ WD.attr input "value" `shouldBeWithRetryM` Just "pb"
568+ shouldContainText "pb" p
569+ WD.sendKeys "abc" input
570+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
571+ shouldContainText "pb" p -- It won't be updated yet
572+ readRef inputRef `shouldBeWithRetryM` [] -- Should never fire input ref during SSR
573+ readRef valRef `shouldBeWithRetryM` ["pb"]
574+ pure (input, p)
575+ checkHydrated (input, p) = do
576+ readRef inputRef `shouldBeWithRetryM` ["pbabc"]
577+ readRef valRef `shouldBeWithRetryM` ["pbabc", "pb"]
578+ WD.attr input "value" `shouldBeWithRetryM` Just "pbabc"
579+ shouldContainText "pbabc" p
580+ pure ()
581+ testWidget' checkStatic checkHydrated $ do
582+ liftIO $ writeRef valRef []
583+ pb <- getPostBuild
584+ e <- inputElement $ def & inputElementConfig_setValue .~ ("pb" <$ pb)
585+ el "p" $ dynText $ _inputElement_value e
586+ performEvent_ $ consRef valRef <$> updated (_inputElement_value e)
587+ performEvent_ $ consRef inputRef <$> _inputElement_input e
419588
420589 describe "hydration/immediate" $ session' $ do
421590 it "captures user input after switchover" $ runWD $ do
@@ -587,6 +756,88 @@ tests withDebugging wdConfig caps _selenium = do
587756 e <- textAreaElement $ def { _textAreaElementConfig_setValue = Just setValue' }
588757 performEvent_ $ liftIO . writeRef valueByUIRef <$> _textAreaElement_input e
589758 performEvent_ $ liftIO . writeRef valueRef <$> updated (value e)
759+ it "fires _input event if the user altered the value before hydration" $ runWD $ do
760+ textarea <- newRef ("" :: Text)
761+ update <- newRef ("" :: Text)
762+ let checkStatic = do
763+ e <- findElemWithRetry $ WD.ByTag "textarea"
764+ WD.attr e "value" `shouldBeWithRetryM` Just ""
765+ WD.sendKeys "test" e
766+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
767+ pure e
768+ checkHydrated e = do
769+ WD.attr e "value" `shouldBeWithRetryM` Just "test"
770+ readRef textarea `shouldBeWithRetryM` "test"
771+ readRef update `shouldBeWithRetryM` "test"
772+ testWidget' checkStatic checkHydrated $ do
773+ e <- textAreaElement def
774+ performEvent_ $ liftIO . writeRef textarea <$> _textAreaElement_input e
775+ performEvent_ $ liftIO . writeRef update <$> updated (_textAreaElement_value e)
776+ it "does not fire _input event when the value is updated at postBuild" $ runWD $ do
777+ textarea <- newRef (Nothing :: Maybe Text)
778+ let checkStatic = do
779+ e <- findElemWithRetry $ WD.ByTag "textarea"
780+ WD.attr e "value" `shouldBeWithRetryM` Just "pb"
781+ pure e
782+ checkHydrated e = do
783+ WD.attr e "value" `shouldBeWithRetryM` Just "pb"
784+ readRef textarea `shouldBeWithRetryM` Nothing
785+ testWidget' checkStatic checkHydrated $ do
786+ pb <- getPostBuild
787+ e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb)
788+ performEvent_ $ liftIO . writeRef textarea . Just <$> _textAreaElement_input e
789+ it "SSR produces correct DOM based on textAreaElement values when setValue happens at postBuild" $ runWD $ do
790+ let checkBoth = do
791+ textarea <- findElemWithRetry $ WD.ByTag "textarea"
792+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pb"
793+ p <- findElemWithRetry (WD.ByTag "p")
794+ shouldContainText "pb" p
795+ testWidget checkBoth checkBoth $ do
796+ pb <- getPostBuild
797+ e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb)
798+ el "p" $ dynText $ _textAreaElement_value e
799+ it "does not fail when both setValue AND user updated value happen before switchover" $ runWD $ do
800+ let checkStatic = do
801+ textarea <- findElemWithRetry $ WD.ByTag "textarea"
802+ h2_value <- findElemWithRetry (WD.ByTag "h2")
803+ h3_input <- findElemWithRetry (WD.ByTag "h3")
804+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pb"
805+ shouldContainText "pb" h2_value
806+ shouldContainText "" h3_input
807+ WD.sendKeys "abc" textarea
808+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc"
809+ shouldContainText "pb" h2_value
810+ shouldContainText "" h3_input
811+ pure (textarea, h2_value, h3_input)
812+ checkHydrated (textarea, h2_value, h3_input) = do
813+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc"
814+ shouldContainText "pbabc" h3_input
815+ shouldContainText "pbabc pb" h2_value
816+ pure ()
817+ testWidget' checkStatic checkHydrated $ do
818+ pb <- getPostBuild
819+ e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb)
820+ el "h1" $ dynText $ _textAreaElement_value e
821+ el "h2" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ updated $ _textAreaElement_value e
822+ el "h3" $ dynText . fmap T.unwords <=< foldDyn (:) [] $ _textAreaElement_input e
823+ it "value is correct when both setValue AND user updated value happen before switchover" $ runWD $ do
824+ let checkStatic = do
825+ textarea <- findElemWithRetry $ WD.ByTag "textarea"
826+ p <- findElemWithRetry (WD.ByTag "p")
827+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pb"
828+ shouldContainText "pb" p
829+ WD.sendKeys "abc" textarea
830+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc"
831+ shouldContainText "pb" p -- It won't be updated yet
832+ pure (textarea, p)
833+ checkHydrated (textarea, p) = do
834+ WD.attr textarea "value" `shouldBeWithRetryM` Just "pbabc"
835+ shouldContainText "pbabc" p
836+ pure ()
837+ testWidget' checkStatic checkHydrated $ do
838+ pb <- getPostBuild
839+ e <- textAreaElement $ def & textAreaElementConfig_setValue .~ ("pb" <$ pb)
840+ el "p" $ dynText $ _textAreaElement_value e
590841
591842 describe "hydration/immediate" $ session' $ do
592843 it "captures user input after switchover" $ runWD $ do
@@ -1008,7 +1259,9 @@ tests withDebugging wdConfig caps _selenium = do
10081259 _ <- runWithReplace (text "inner1" *> comment "replace-end-0") $ text "inner2" <$ replace
10091260 text "|after"
10101261 void $ runWithReplace blank $ el "p" blank <$ replace -- Signal tag for end of test
1011− it "ignores missing ending bracketing comments" $ runWD $ do
1262+ -- TODO This test actually causes a hydration failure, but it wasn't
1263+ -- previously detected, so I've marked it pending
1264+ xit "ignores missing ending bracketing comments" $ runWD $ do
10121265 replaceChan :: Chan () <- liftIO newChan
10131266 let
10141267 preSwitchover = do
@@ -1338,7 +1591,8 @@ tests withDebugging wdConfig caps _selenium = do
13381591 shouldContainText "before\ninner\nafter" p1
13391592 elementShouldBeRemoved ol
13401593 elementShouldBeRemoved p2
1341− testWidget' preSwitchover check $ do
1594+ -- Don't fail fatally when hydration encounters the invalid DOM
1595+ testWidgetDebug' False withDebugging preSwitchover check $ do
13421596 -- This is deliberately invalid HTML, the browser will interpret it as
13431597 -- <p>before</p><ol>inner</ol>after<p></p>
13441598 el "p" $ do
@@ -1464,11 +1718,12 @@ testWidgetStaticDebug
14641718 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
14651719 -- ^ Widget we are testing
14661720 -> WD b
1467−testWidgetStaticDebug withDebugging w = testWidgetDebug withDebugging (void w) w
1721+testWidgetStaticDebug withDebugging w = testWidgetDebug True withDebugging (void w) w
14681722
14691723-- | TODO: do something about JSExceptions not causing tests to fail
14701724testWidgetDebug
14711725 :: Bool
1726+ -> Bool
14721727 -> WD ()
14731728 -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
14741729 -> WD b
@@ -1476,12 +1731,16 @@ testWidgetDebug
14761731 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
14771732 -- ^ Widget we are testing
14781733 -> WD b
1479−testWidgetDebug withDebugging beforeJS afterSwitchover =
1480− testWidgetDebug' withDebugging beforeJS (const afterSwitchover)
1734+testWidgetDebug hardFailure withDebugging beforeJS afterSwitchover =
1735+ testWidgetDebug' hardFailure withDebugging beforeJS (const afterSwitchover)
1736+
1737+data HydrationFailedException = HydrationFailedException deriving Show
1738+instance Exception HydrationFailedException
14811739
14821740-- | TODO: do something about JSExceptions not causing tests to fail
14831741testWidgetDebug'
14841742 :: Bool
1743+ -> Bool
14851744 -> WD a
14861745 -- ^ Webdriver commands to run before the JS runs (i.e. on the statically rendered page)
14871746 -> (a -> WD b)
@@ -1489,7 +1748,7 @@ testWidgetDebug'
14891748 -> (forall m js. TestWidget js (SpiderTimeline Global) m => m ())
14901749 -- ^ Widget we are testing (contents of body)
14911750 -> WD b
1492−testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
1751+testWidgetDebug' hardFailure withDebugging beforeJS afterSwitchover bodyWidget = do
14931752 let putStrLnDebug :: MonadIO m => Text -> m ()
14941753 putStrLnDebug m = when withDebugging $ liftIO $ putStrLn $ T.unpack m
14951754 staticApp = do
@@ -1501,6 +1760,7 @@ testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
15011760 ((), html) <- liftIO $ renderStatic $ runHydratableT staticApp
15021761 putStrLnDebug "rendered static"
15031762 waitBeforeJS <- liftIO newEmptyMVar -- Empty until JS should be run
1763+ onFailure <- if hardFailure then (\tid -> throwTo tid HydrationFailedException) <$> liftIO myThreadId else pure $ pure ()
15041764 waitUntilSwitchover <- liftIO newEmptyMVar -- Empty until switchover
15051765 let entryPoint = do
15061766 putStrLnDebug "taking waitBeforeJS"
@@ -1512,7 +1772,7 @@ testWidgetDebug' withDebugging beforeJS afterSwitchover bodyWidget = do
15121772 liftIO $ putMVar waitUntilSwitchover ()
15131773 putStrLnDebug "put waitUntilSwitchover"
15141774 putStrLnDebug "running mainHydrationWidgetWithSwitchoverAction"
1515− mainHydrationWidgetWithSwitchoverAction switchOverAction blank bodyWidget
1775+ mainHydrationWidgetWithSwitchoverAction onFailure switchOverAction blank bodyWidget
15161776 putStrLnDebug "syncPoint after mainHydrationWidgetWithSwitchoverAction"
15171777 syncPoint
15181778 application <- liftIO $ jsaddleOr defaultConnectionOptions entryPoint $ \_ sendResponse -> do