fp-ts: Development of fp-ts-webdriver
Tags: fpts, typescript, functional, webdriver
Published: 2021-02-17 08:05:05

<small><span>Phenomenal Photo by <a href="https://unsplash.com/@tokeller?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Tobias Keller</a> on <a href="https://unsplash.com/s/photos/red-hot-australia?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span></small>

I've been looking to use fp-ts to communicate with a webdriver, called [`fp-ts-webdriver`](https://github.com/waynevanson/fp-ts-webdriver). I love the API because it allows the reduction of side effects and creates testable code.


Here is a non-exhaustive list of challenges across the way, hoping the problem solving process may aid you in your challenges.

## Why

There are so many webdriver frameworks out in the abyss. Why bother writing another? Why not just wrap an existing client side API in fp-ts?

I already tried it. It sucks:
- Imperative code is hard to follow (now that I've indulged in functional programming).
- The `async/await` syntax is terrible to work with. `then().then().then()`
- Because promises aren't lazy, they cannot be easily composed.

## Challenges

### Understanding the Webdriver

The webdriver protocol is a specification written by W3C, the officially standards body of the web. The specification indicates a client-server relationship, just like when we send a message to a server and the server sends back information, like a webpage. We are the client (or local end) and the server is the remote end.

The client sends a request (a webdriver command) to the remote web driver, as advised in the specification. The remote end processes the request, performs the actions you have asked, then sends back an error or a success response, as advised in the specification.

As a web developer, you're likely to have seen the client-server relationship in action many times prior.

### Choosing how to compose the API

#### Asynchonous Composition

In fp-ts and functional programming, we use monads to compose actions.

In our case, we want to:
- Compose the sending of requests to the remote webdriver (server).
  -  Send a request.
  -  Receive the response.
    - If a successful response (200), send the next request
    - If it's an unsuccessful response (`<200 && >=300`), return an error (without throwing of course) and do not send another request issued by the user. 
  - ALWAYS cleanup any resources (eg. An open session)

I knew I was going to use the `fetch` API to send a request, which returns a promise that may return an error. The monad of choice is `TaskEither`.  A `Task` is a lazy promise and an `Either` is monad with 2 mutually present values, meaning it's either one value or the other.

Here's an article I wrote to prime you for Either.

#### Dependency Injection

We have a few values we don't quite know their exact values as of yet. I want to expose these as options for the user, so they can choose the `endpoint` (url) and change the `RequestInit` so they could use a proxy or do any other HTTP configuration that I don't know of yet.

In fp-ts, use  the `Reader` monad for composing dependency injection. All it is a function that always takes the same argument `<R, A>(dependency: R) => A`. To inject more than one, use a tuple or an object. We'll use an object so we can name our properties.

Our monad is now `ReaderTaskEither`, where the `Reader` is wrapped around `TaskEither`.
This feels good! Can we go further?

#### Session Composition

YES! We can go further.

I noticed that the session was always an argument in the functions I needed. Not in all of it, but with 80% of the functions. This is a dependency. You know what we do with dependencies? We apply (inject) them!

Instead of merging our dependencies, let's keep them separate. Refactored, we have now created a new monad, never before seen by fp-ts: `ReaderReaderTaskEither`.

It sounds like we're going over the top, and it does seem like we've gone bananas...

But you wanted to control a browser right? That's a lot of responsibility. We need the power of functional programming!

With great responsibility comes great power.

### Runtime type safety

We know the shape that the response should be in `Success<A> = {value: A} `, where `A` could be anything. When we know the shape of A at compile time, how do we ensure that every response we use is the right type at runtime? Imagine getting response `500` instead of `200`, in which the responses body is not type `Success<A>`. Ouch. 

If you know the fp-ts ecosystem well, you'll know we're using our boy `io-ts` to ensure that the typescript you see in your project will be GUARANTEED at runtime. We'll be using the `Decode` module.

This uses the `Either` monad ,as discussed earlier. This lifts easily into `*TaskEither` via Kleisli Composition, which we'll use internally.

### Boilerplate

This is where we start to apply all the previous content in this post.

After a while, we start to see a pattern emerge. We're copying and pasting a lot of code. Some copy and paste is expected, as we're writing a uniform API. However, you'll know what to refactor to after doing it **6 TIMES**.

If you get one take from this, it is that you must **repeatedly write code to truly understand how and why you're able to refactor it**.

I created a `make` function, which takes a `decoder`, `method`, `endomorphicURL` and maybe a `body`. Endomorphism is `<A>(a: A) => A`; The input type is the same as the output type. We use a string for this property.

### Reading a Specification

Now specifications are supposed to clear and normative. Let's try decipher one together.

> The remote end steps are:
>
> 1. Let data be the result of getting a property named cookie from the parameters argument. 
> 2. If data is not a JSON Object with all the required (non-optional) JSON keys listed in the table for cookie conversion, return error with error code invalid argument. 
>
> \- [Add Cookie](https://w3c.github.io/webdriver/#add-cookie)

The server (remote) expects to see at least the following the body of the response: `{ cookie: { name: "", value: "", path?: "", ... rest } }`.
I won't list the rest, but you can see them in the [table for cookie conversion](https://w3c.github.io/webdriver/#dfn-table-for-cookie-conversion)

It's a lot of jargon, but after a while it's like a second language.

### Testing a webdriver

This hit me for a six at the start.

In jest, we need to call `beforeAll()`  to spin up a webdriver (chromedriver in our case) and `afterAll()` to spind down the webdriver.

On my local machine, it opens up an actual browser and I can see it do the tests. What if we don't want that when it's running on CI? Welcome to headless.

I had to squeeze together a bunch of resources to find this fix, so here's the code:

```ts
const capabilities: Capabilities = {
  alwaysMatch: {
    "goog:chromeOptions": { args: ["--headless"] },
  },
}
```

Now all the setup is done, I can just write a test for a feature.
If it works, it's ready to ship!

### Publishing

I've published a few libraries, but I knew I needed to automate this one.

I conjured up GitHub Actions alongside `semantic-release`. The only thing I have to do is use `git` correctly, run `git pull --rebase` before running `git push` in order to get. You should be doing this anyway, depending on your workflow.

I get an iterative change log, a published NPM package and a release on GitHub without even having to think about publishing. Phenomenal!