Not sure if it is too early to ask about "idiomatic" ways of writing Roc code, but here we go... :D
I'm playing with a small "End2End Tests" package in Roc that is using the WebDriver (selenium) underneath.
But the raw web client needs the sessionId as a parameter with every request:
runTest = \{} ->
sessionId <- WebDriver.startSession {} |> await
_ <- WebDriver.navigateTo sessionId "https://google.pl" |> await
button1 <- WebDriver.findElement sessionId (Css "#submit-button") |> await
_ <- WebDriver.clickElement sessionId button1 |> await
Would it be acceptable in Roc to return a record with functions that have the sessionId
in closure? like this:
runTest = \{} ->
r2E <- Roc2End.startSession "https://google.pl" |> await
button1 <- r2E.findElement (Css "#submit-button") |> await
_ <- r2E.clickElement button1 |> await
The second snippet is easier to use and easier to read, but it seems like "cheating" and artificially creating "class objects"...
Would this be considered an anti-pattern? Or is it ok?
// btw. I think that in haskell you would use a monad to hide the sessionId
I think you can also do something monadic in roc if you want. I think roc random has a similar API to avoid needing to pipe the state around everywhere
That said, I think the closures are pretty reasonable as long as the session won't need to get updated.
a relevant question here is: do you ever need to run a Task
in between any of these? Or can it be just "run all the webdriver steps one after the other, with no Task
s ever being run in between any step" and that's sufficient?
in most use cases when using a webdriver you probably want to wait for the result before continuing.
e.g. you need to get the id of the button before you can use it to send a "click":
button1 <- WebDriver.findElement sessionId (Css "#submit-button") |> await
WebDriver.clickElement sessionId button1
or get the value of an input and then decide what to do next.
But to be fair - this is not a great problem to be solved using a pure functional language.
Everything you do with a webdriver is just side effects :grinning:
I'm just using the Http api from the basic-cli platform for this...
But I think that you could create a nice api and a great user experience even for end-to-end tests, by creating a dedicated e2e platform.
Can't wait to see what amazing platforms people will create in the future :heart:
oh I think this is a fine use case!
I'm specifically asking about Task
because it has implications for what your options are for handling the session
waiting for the result before continuing is totally fine, but it matters whether after you get that result you might want to run an arbitrary Task
(like writing to a file or something) before doing the next webdriver action
personally the way I've written tests like this in the past, I've never needed that, but I'm not sure if the use cases you have in mind are different!
(e.g. maybe a particularly customizable scraper might want to do this, I could imagine, but if it's just for e2e tests, maybe not)
you're right!
when writing e2e test I don't need to run any tasks between the steps.
not sure if this is what you had on your mind, but I could create pure functions and a pure data structure to write the tests, and then a function e.g. "runTest" would take the data structure and interpret it to make the http calls and run the tasks.
Almost like pushing out the impure code to the edges of my program.
idk, will try to experiment with that.
yeah exactly!
there are a couple of ways to do that, and they have different tradeoffs in terms of how the API feels to use and how the under-the-hood data structure looks
for example, there's a way to make the API look and feel like Task
, while having an interpretable structure under the hood, but it looks strange under the hood and I don't think I ever would have guessed how to do it if someone hadn't explained the technique to me, so let me know if that's of interest! :big_smile:
Last updated: Jul 05 2025 at 12:14 UTC