Has anyone used Halogen with jsdom?

My company’s product is built on PureScript Halogen and I’d like to use screenshots in documentation/blogs.

One approach I’m exploring is to load up a specialized page in my product at compile-time, run my SPA in nodejs+jsdom, trigger a series of events (user typed here, clicked that) via Halogen’s query algebra, and then take a snapshot of the DOM at that exact moment, which I can embed in my site’s pages or blog with the appropriate CSS. I could then generate literal “step-by-step” howtos.

This is also relevant for testing. I’d like to trigger events against Halogen components and check that they remain in a consistent state. One could even QuickCheck-style generate random events and see whether it breaks.

I’d have to patch Halogen to expose some hooks into components to do that, but I’d make light work of that.

jsdom is a JS implementation of the DOM and other APIs that are exposed in the browser to the JS engine: https://github.com/jsdom/jsdom

I think this would be a really good avenue to look into. I want to avoid having to run a full browser implementation–which other approaches take–in my test suite. I may do that one day; it’s just super error prone in my experience. Whereas a locked down node binary and running a headless PureScript Halogen component seems more reliable.

Anyone tried it?

4 Likes

This may be relevant - https://github.com/ajnsit/purescript-halogen-vdom-independent

I have extracted and abstracted all the DOM mutation parts from the underlying halogen-vdom engine using a HostConfig typeclass. The HostConfig itself is modeled after React’s HostConfig and can theoretically be implemented for any backend, including jsdom.

I don’t know how big an effort integrating this version of halogen-vdom into the larger halogen ecosystem would be though.

2 Likes

Thanks for sharing.

jsdom acts as a drop in replacement for the browser DOM — so I think it ought to be possible to run unmodified halogen against it, no?

It seems like your generalized vdom backend work would be useful if I wanted to get at the render trees more directly without the DOM tree intermediary, right?

1 Like

You can get at the VDOM tree already (for example https://github.com/purescript-halogen/purescript-halogen-vdom-string-renderer). The generalized backend would be if you wanted some other graphical backend that utilizes the existing diffing logic in halogen-vdom.

2 Likes

So, writing up the options in my own words:

  1. Use the halogen-vdom-string-render for Node-based server-side rendering, or possibly renderer test suites. May encounter trouble if your component needs the DOM API to render properly (e.g. using CodeMirror).
  2. Use halogen-vdom-independent if I wanted to e.g. use Halogen on a canvas tag or sdl2 or gtk+ or vty (e.g. brick) – i.e. an environment that is not DOM-based, and possibly also for server-side rendering as a backend.
  3. Use jsdom + regular halogen to run Halogen in Node for server-side rendering, or test suites. In particular, use this when your components actively make use of the DOM API for functionality (e.g. using Ref’s or interacting with third-party libs).
1 Like

Does this only get to the top level static part of the tree? As far as I can see there’s no way to actually run a test suite (which invokes handlers) with this renderer.

Also you still need access to a context where the DOM APIs work.

1 Like

Essentially it can be used for anything where you want more control over how UI fragments are created, modified, and removed. Including targeting non-dom backends.

1 Like

I’m not suggesting to write tests in terms of string-renderer, just to show that there is some prior work of consuming the VDom type directly for a purpose other than running an application against the DOM. Components in Halogen are “just” a record of some initial state paired with a transition function, so it can be unfolded arbitrarily deep at the very least based on the initial state. I think the HostConfig record should only be necessary if you want to observe some property of the actual diffing process. Otherwise, it should possible to write a test interpreter for HalogenM, and way to query the VDom itself in a “pure” manner depending upon what is actually feasible to test like this. Obviously if you have a lot of raw Effect and Aff code, or raw DOM code, you might have a hard time with this. But that’s likely better suited for integration testing anyway.

1 Like

I mean, to unfold the components you would need to run the VDom machine which would run all the DOM API methods. That’s my understanding anyways. Am I missing something fundamental? If I knew this was possible, I would probably not have written halogen-vdom-independent.

There are two things at play:

  • The VDom a w type, which is what you produce in some render function. This is just a tree data structure which can be traversed and analyzed. In Halogen specifically, the w type will that of components (or thunks). This type is independent of any backend.
  • The VDomMachine a w type, which is the stateful process that accepts a VDom a w, and produces a built Node. VDomMachine specifically is typed to the DOM API (it’s in the Halogen.VDom.DOM namespace), and it knows how to construct and patch a concrete DOM tree.

You can walk VDom a w on it’s own without pulling in Halogen.VDom.DOM.VDomMachine. For Halogen specifically, you can case on the Widget constructor, pull out the ComponentSlot, and continue to expand the tree however you need to. For example, you could produce a new, fully expanded VDom tree of initial renders, without any components in the tree, and query it (if one were to write some library to do that).

I spent an hour and wrote up my findings. jsdom+node does work with Halogen, based on my initial test.

Here’s a gist documenting my test. I do recommend the docker approach, because the node/js ecosystem is a house of cards; I wasted plenty of time on that; freezing it into a docker image manages some of that chaos.

Next step is to add some simple hook to Halogen, such as:

  1. Tell me when you’ve completed a render.
    a. Presently, I’m using a MutationObserver which gives me a rough guide.
  2. Trigger this command FooBar 123 at component slot X/Y/Z in a hierarchical manner.
    a. Alternatively, I could look at how hard it is to use jsdom to dispatch events to certain DOM elements, which may be a more “integrationy” test.
4 Likes

Yup that’s what I meant by the static widget. You can’t run the actual dom diffing functionality without running the dom api.