Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to JSDOM #1

Merged
merged 1 commit into from
Sep 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1,256 changes: 869 additions & 387 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
},
"license": "MIT",
"dependencies": {
"@happy-dom/global-registrator": "^6.0.2",
"global-jsdom": "^8.0.0",
"jsdom": "^16.5.1",
"bower": "^1.8.14",
"esbuild": "^0.14.48",
"pulp": "^16.0.2",
Expand Down
1 change: 1 addition & 0 deletions spago.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
, "exceptions"
, "foldable-traversable"
, "maybe"
, "nullable"
, "prelude"
, "spec"
, "transformers"
Expand Down
4 changes: 3 additions & 1 deletion src/Elmish/Test.purs
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,15 @@
module Elmish.Test
( module Elmish.Test.Bootstrap
, module Elmish.Test.Combinators
, module Elmish.Test.Discover
, module Elmish.Test.Events
, module Elmish.Test.Query
, module Elmish.Test.SpinWait
) where

import Elmish.Test.Bootstrap (testComponent, testElement)
import Elmish.Test.Combinators (chain, chainM, forEach, mapEach, within, within', (##), ($$), (>>))
import Elmish.Test.Discover (childAt, children, find, findAll, findFirst, findNth)
import Elmish.Test.Events (change, click, clickOn, fireEvent)
import Elmish.Test.Query (attr, exists, find, findAll, findFirst, findNth, html, prop, tagName, text)
import Elmish.Test.Query (attr, count, exists, html, prop, tagName, text)
Comment on lines +105 to +107
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I broke up the Query module into Query (reading properties of elements) and Discover (manipulating/traversing the DOM tree). Initially it was necessary for my implementation of innerText to avoid cyclic imports. I changed the implementation since then, but decided to leave the separation.

import Elmish.Test.SpinWait (waitUntil, waitUntil', waitWhile, waitWhile')
4 changes: 2 additions & 2 deletions src/Elmish/Test/Bootstrap.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GlobalRegistrator } from '@happy-dom/global-registrator';
import registerDom from 'global-jsdom'

export const ensureDom_ = () => {
if (typeof window === "undefined") {
GlobalRegistrator.register()
registerDom()
}
}
2 changes: 1 addition & 1 deletion src/Elmish/Test/Combinators.purs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Prelude

import Control.Monad.Reader (local)
import Data.Traversable (traverse, traverse_)
import Elmish.Test.Query (find)
import Elmish.Test.Discover (find)
import Elmish.Test.State (class Testable, TestState(..))
import Web.DOM (Element)

Expand Down
102 changes: 102 additions & 0 deletions src/Elmish/Test/Discover.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
module Elmish.Test.Discover
( childAt
, children
, find
, findAll
, findFirst
, findNth
)
where

import Prelude

import Data.Array (fold, length, mapMaybe, (!!))
import Data.Maybe (Maybe(..))
import Effect.Class (liftEffect)
import Elmish.Test.State (class Testable, crash, currentNode)
import Web.DOM (Element)
import Web.DOM.Element as DOM
import Web.DOM.Node (childNodes)
import Web.DOM.NodeList as NodeList
import Web.DOM.ParentNode (QuerySelector(..), querySelectorAll)

-- | Finds exactly one element by CSS selector. If the selector matches zero
-- | elements or more than one, this function will throw an exception.
-- |
-- | find "button" >> click
-- |
find :: ∀ m. Testable m => String -> m Element
find selector =
findAll selector >>= case _ of
[el] -> pure el
els -> crash $ "Expected to find one element matching '" <> selector <> "', but found " <> show (length els)

-- | Finds the first element out of possibly many matching the given selector.
-- | If there are no elements matching the selector, throws an exception.
findFirst :: ∀ m. Testable m => String -> m Element
findFirst = findNth 0


-- | Finds the n-th (zero-based) element out of possibly many matching the given
-- | selector. If there are no elements matching the selector, throws an
-- | exception.
findNth :: ∀ m. Testable m => Int -> String -> m Element
findNth idx selector =
findAll selector >>= \all -> case all !! idx of
Just el -> pure el
Nothing -> crash $ fold
[ "Expected to find "
, show idx
, "th element matching '"
, selector
, "', but there are only "
, show (length all)
, " elements"
]

-- | Finds zero or more elements by CSS selector.
-- |
-- | findAll "button" >>= traverse_ \b -> click $$ b
-- |
-- | divs <- find "div"
-- | length divs `shouldEqual` 10
-- |
findAll :: ∀ m. Testable m => String -> m (Array Element)
findAll selector = do
current <- currentNode
liftEffect $
querySelectorAll (QuerySelector selector) (DOM.toParentNode current)
>>= NodeList.toArray
<#> mapMaybe DOM.fromNode

-- | Returns all immediate child elements of the current-context element.
-- |
-- | find "div" >> children >>= traverse_ \child ->
-- | tag <- tagName
-- | when (tag == "BUTTON")
-- | click
-- |
children :: ∀ m. Testable m => m (Array Element)
children = do
current <- currentNode
liftEffect $
childNodes (DOM.toNode current)
>>= NodeList.toArray
<#> mapMaybe DOM.fromNode

-- | Within the current-context element, finds a child element at the given
-- | index. Crashes if the is no child with the given index.
childAt :: ∀ m. Testable m => Int -> m Element
childAt idx = do
cs <- children
case cs !! idx of
Just e ->
pure e
Nothing ->
crash $ fold
[ "Expected to find a child element at index "
, show idx
, ", but there are only "
, show (length cs)
, " children"
]
2 changes: 0 additions & 2 deletions src/Elmish/Test/DomProps.purs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ newtype DomProp (a :: Type) = DomProp String

value = DomProp "value" :: DomProp String

href = DomProp "href" :: DomProp String
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out this is a not a property. Only accessible via getAttribute.


disabled = DomProp "disabled" :: DomProp Boolean

checked = DomProp "checked" :: DomProp Boolean
2 changes: 1 addition & 1 deletion src/Elmish/Test/Events.purs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Effect.Class (liftEffect)
import Effect.Uncurried (EffectFn3, runEffectFn3)
import Elmish.Foreign (class CanPassToJavaScript)
import Elmish.Test.Combinators ((>>))
import Elmish.Test.Query (find)
import Elmish.Test.Discover (find)
import Elmish.Test.State (class Testable, currentNode)
import Web.DOM (Element)

Expand Down
1 change: 0 additions & 1 deletion src/Elmish/Test/Query.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export const innerText_ = e => e.innerText || ""
export const outerHTML_ = e => e.outerHTML || ""
export const prop_ = (name, e) => e[name] || null // converting `undefined` to `null` so it can be handled via `Nullable`
108 changes: 12 additions & 96 deletions src/Elmish/Test/Query.purs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
module Elmish.Test.Query
( attr
, childAt
, children
, count
, exists
, find
, findAll
, findFirst
, findNth
, html
, prop
, tagName
Expand All @@ -16,108 +11,31 @@ module Elmish.Test.Query

import Prelude

import Data.Array (fold, length, mapMaybe, null, (!!))
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Array (length, null)
import Data.Maybe (fromMaybe)
import Data.Nullable as N
import Effect.Class (liftEffect)
import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2)
import Elmish.Test.Discover (findAll)
import Elmish.Test.DomProps (class DomPropType, DomProp, defaultValue)
import Elmish.Test.State (class Testable, crash, currentNode)
import Elmish.Test.State (class Testable, currentNode)
import Web.DOM (Element)
import Web.DOM.Element as DOM
import Web.DOM.Node (childNodes)
import Web.DOM.NodeList as NodeList
import Web.DOM.ParentNode (QuerySelector(..), querySelectorAll)

-- | Finds exactly one element by CSS selector. If the selector matches zero
-- | elements or more than one, this function will throw an exception.
-- |
-- | find "button" >> click
-- |
find :: ∀ m. Testable m => String -> m Element
find selector =
findAll selector >>= case _ of
[el] -> pure el
els -> crash $ "Expected to find one element matching '" <> selector <> "', but found " <> show (length els)

-- | Finds the first element out of possibly many matching the given selector.
-- | If there are no elements matching the selector, throws an exception.
findFirst :: ∀ m. Testable m => String -> m Element
findFirst = findNth 0


-- | Finds the n-th (zero-based) element out of possibly many matching the given
-- | selector. If there are no elements matching the selector, throws an
-- | exception.
findNth :: ∀ m. Testable m => Int -> String -> m Element
findNth idx selector =
findAll selector >>= \all -> case all !! idx of
Just el -> pure el
Nothing -> crash $ fold
[ "Expected to find "
, show idx
, "th element matching '"
, selector
, "', but there are only "
, show (length all)
, " elements"
]

-- | Finds zero or more elements by CSS selector.
-- |
-- | findAll "button" >>= traverse_ \b -> click $$ b
-- |
-- | divs <- find "div"
-- | length divs `shouldEqual` 10
-- |
findAll :: ∀ m. Testable m => String -> m (Array Element)
findAll selector = do
current <- currentNode
liftEffect $
querySelectorAll (QuerySelector selector) (DOM.toParentNode current)
>>= NodeList.toArray
<#> mapMaybe DOM.fromNode

-- | Returns all immediate child elements of the current-context element.
-- |
-- | find "div" >> children >>= traverse_ \child ->
-- | tag <- tagName
-- | when (tag == "BUTTON")
-- | click
-- |
children :: ∀ m. Testable m => m (Array Element)
children = do
current <- currentNode
liftEffect $
childNodes (DOM.toNode current)
>>= NodeList.toArray
<#> mapMaybe DOM.fromNode

-- | Within the current-context element, finds a child element at the given
-- | index. Crashes if the is no child with the given index.
childAt :: ∀ m. Testable m => Int -> m Element
childAt idx = do
cs <- children
case cs !! idx of
Just e ->
pure e
Nothing ->
crash $ fold
[ "Expected to find a child element at index "
, show idx
, ", but there are only "
, show (length cs)
, " children"
]
import Web.DOM.Node (textContent)

-- | Returns `true` if at least one element exists matching the given CSS
-- | selector.
exists :: ∀ m. Testable m => String -> m Boolean
exists selector = not null <$> findAll selector

-- | Returns the number of elements within the current context that match the
-- | given selector.
count :: ∀ m. Testable m => String -> m Int
count selector = length <$> findAll selector
Comment on lines +33 to +34
Copy link
Contributor Author

@fsoikin fsoikin Sep 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out we're utilizing this in our codebase a lot, I had to convert all those places to findAll "..." >>= Array.length, which is less readable. So I decided to add this utility here.


-- | Returns full inner text of the current-context element.
text :: ∀ m. Testable m => m String
text = currentNode >>= (liftEffect <<< runEffectFn1 innerText_)
text = currentNode >>= \el -> liftEffect $ textContent (DOM.toNode el)

-- | Returns HTML representation of the current-context element.
html :: ∀ m. Testable m => m String
Expand All @@ -141,8 +59,6 @@ prop :: ∀ m a. Testable m => DomPropType a => DomProp a -> m a
prop name = currentNode >>= \e -> liftEffect $
runEffectFn2 prop_ name e <#> N.toMaybe <#> fromMaybe defaultValue

foreign import innerText_ :: EffectFn1 Element String

foreign import outerHTML_ :: EffectFn1 Element String

foreign import prop_ :: ∀ a. EffectFn2 (DomProp a) Element (N.Nullable a)
3 changes: 1 addition & 2 deletions test/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import Elmish.Test.DomProps as P
import Elmish.Test.Events (change, click, clickOn)
import Test.Spec (Spec, describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Assertions.String (shouldContain)
import Test.Spec.Reporter (consoleReporter)
import Test.Spec.Runner (runSpec)

Expand Down Expand Up @@ -51,7 +50,7 @@ spec =
change "Frodo"
prop P.value >>= shouldEqual "Frodo"

text >>= (_ `shouldContain` "Hello, Frodo")
text >>= shouldEqual "1IncDecHello, Frodo"

-- findAll
buttons <- findAll "button"
Expand Down