There's been a lot of motion in the .Net ecosystem overall the couple years, first with the announcement of .Net Core as a runtime and then with the rising popularity of the dotnet SDK and CLI as a way of interacting with your projects. While all the old knowledge about MSBuild and projects is still viable, I thought that I'd walk through some of the habits and practices I use to build libraries in this new work. To do this, I started making Figurine.
A lot of this post is covered in a live stream that I did a week ago, so if you learn more visually you can go to the Twitch stream or the YouTube version to check it out.
Motivation
In the F# community we all know and love Canopy, the library that provides a nice DSL for orchestrating Selenium. In fact, my post two years ago about converting the F# UserVoice Suggestions to GitHub used Canopy to power that whole migration. But these days we have a choice in browser automation tools, and the new kid on the block is Puppeteer, a tool that uses WebSockets to drive instances of Chrome(ium) using the Debug Tools API. My thought was that it would be easy to create a library that had the same ease of use as Canopy but for this Puppeteer, and thus Figurine was born.
Setup
The first thing I knew I'd need is a good Continuous Integration pipeline. If Figurine was going to be a successful project, I needed not just the library itself but a consistent way to test, build, package, check, and publish the library, if for no other reason than to ensure that maintenance of the library wouldn't be a hassle to me. For this purpose I naturally chose to base the library off of Jimmy Byrd's MiniScaffold project. This project is a template for the dotnet new CLI command that provides a laundry list of features including
- a full-featured build/CI pipeline with FAKE 5
- consistent, repeatable package dependency management with Paket
- automated versioning with RELEASE_NOTES.md
- testing with Expecto
- code coverage with AltCover
- Sourcelinking of symbols/pdbs
- Nuget packaging
- GitHub releases
- code formatting with Fantomas
all without setting anything up other than a name and a GitHub API token. The one major missing point is automated documentation generation and publishing, but there's even a work-in-progress pull request for that work already.
Getting started was a snap, I just had to ensure that I had the latest version of the template installed and then make my new project.
Let's get hacking
Next I had to add the .Net version of the Puppeteer drive, Puppeteer Sharp. This package is very active and exposes a ton of functionality, most of which I just needed to wrap in a way that evoked Canopy. Adding the package was very easy using Paket, I just ran 'mono .paket/paket.exe add PuppeteerSharp -i' and selected to install the package in my main Figurine project.
Once I had the dependency installed, I did a bit of reading in their API documentation. It seemed that there were three major concepts in Puppeteer:
- The 'Browser' is the overall connection to the browser instance that you're driving,
- The 'Page' is your main entry point into interacting with a particular web page, and
- The 'ElementHandle' is the main point for doing interaction with DOM elements, like reading properties, clicking, writing properties, etc.
From this, I thought of a few things immediately:
- I wanted tests to be stateless as much as possible, so no global configuration
- I wanted test to be synchronous by default for easier reading (negotiable)
- I wanted a user to not have to care about the internals of Pages/Browsers/etc.
So I immediately thought of a context/configuration type that would be a parameter to most functions and allow me to abstract the level of the API away from the page/element mechanics:
The idea was to give the user a way to create this semi-opaque BrowserCtx type from a set of LaunchOptions and then free them from handling the minutia of the browser/page duality. In the general case, right now, a user will spawn one BrowserCtx per test and operate on it for the duration of the test, like so:
In the future I'd like to expand on this even further so that you could create a context and spawn multiple pages, one per test, from it more easily, because there's not really a need to create an entire browser. That's just for convenience right now.
Polishing it up
Moving on, I wrote some tests with the functions and parameters I'd need to test some simple functionality, like navigating to a URL, waiting for a particular condition, and finding elements by selectors. Naturally these all broke, but defining my use cases in terms of the end result was helpful in figuring out what kind of information I'd need in the BrowserCtx type. I then started filling in the implementations, using the API documentation for Puppeteer as a guide. It's all very rough now, but I ended up with the following basic functions for version 0.1.0:
- click an element
- get the current url
- find all elements matching a selector
- find a single element by a selector
- run some arbitrary js
- read the value from an element
- navigate to a url
- wait for a condition to be true on the page
- write a value to an element
This was enough for me to do several useful tests, and is a good base to build on. Once I was happy with the API surface, I documented the functions briefly (here's an example from Fuget.org):
It's a start, alright?
At this point all that was left to do in my little library was publish the package to nuget and coin a github release. Thanks to MiniScaffold, this only required three things:
- Establishing an account on Nuget.org (I had already done this)
- Making a Github Personal Access Token to create the release, and setting it to the GITHUB_TOKEN environment variable
- running './build.sh Release'
At the end of that process I could go to Figurine's Nuget page and the GitHub releases page to see the results of my work.
Wrapping up
At the end of this process I got a lot for free from MiniScaffold and its integrations with other dotnet tools:
- Appveyor/Travis CI builds (which are breaking right now because I need an environment-agnostic way to download chrome, but....)
- Release Management
- Code Coverage (checkout the docs/ folder of the repo for the coverage report)
But there's still more that I need to do to really polish off Figurine, not all of which is actual code! I need to
- Write actual documentation and host it automatically
- Work on the nuspec to add in better descriptions, icon, etc
- Get the CI builds working consistently
- Expand the capability of the library itself
But each of these things is doable, and more importantly I got a head-start on all of them _for free_ by using the work of other members of the community. And for the first point, I can work with Jimmy and other maintainers to get documentation-generation into the MiniScaffold template itself and then everyone will get the benefit of docs out-of-the-box.
I'm very glad that it's so easy to get the grunt work of productionalizing a library out of the way these days. Between dotnet sdk templates, the mainstreaming of sourcelink, and the unification onto .netstandard2.0, it's never been less hassle to share your code with others, so get out there and share some of your own!