When we look at a codebase of any deck.gl-based web application, we find several interesting points. First and foremost, at least 50% of all app functionalities are written specifically for visualizing and interacting with 2D or 3D objects on a given map. If we would want to test how the app, map, or those objects behave, there already is an officially supported method – deck.gl's SnapshotTestRunner– which we find to be quite insufficient, since it only allows testing in a “read-only" mode. If your app, like ours, supported features such as drag-and-drop, object selection, live object editing, then you’ll come to the conclusion that something more advanced than snapshot testing is required.
Another side-effect of not being able to test such a web app is extremely low code coverage. Simply put, when there’s no possibility to write tests covering those advanced features mentioned above, nobody can say their deck.gl app has good code coverage.
As one of the most versatile and adaptable solutions available, Cypress is what our QA uses to implement end-to-end tests. That also applies in our deck.gl application, and since the application itself is very large, the other 50% of the functionalities were already covered by Cypress tests (those that don’t deal with any of the deck.gl features).
At that point, we wanted to get into how we might implement all those advanced features without having to setup nor use any additional testing frameworks, so that we wouldn’t have to worry about any overhead.
The answer to this question lies in the very simple fact that deck.gl uses WebGL technologies and features under the hood. With such an implementation strategy in mind, we know that none of the elements we see on the screen are actually DOM elements.
The elements we want to render on a deck.gl map are actually going into a canvas element, which is what makes retrieving them quite difficult. Once all 2D/3D objects are rendered on the canvas (scene), they are hidden away and inaccessible (in other words, you won’t be able to see them even if you explicitly searched for them via the Inspect Element window). When that is the case, not even Cypress can fetch the objects and validate their existence.
Interactive features of our app include actions like enabling / disabling a layer (group of objects) in the scene, moving an object around the scene, hovering object to display a tooltip. Validating those features automatically means we need to get information whether an object is currently displayed in scene, where it’s displayed, in what shape, color, etc. Generally speaking, we needed a way to introspect a scene, a way to find an object (cy.get alternative) and get its properties.
From code perspective introspecting a scene can be seen as getting access to WebGL context, its internal state and operations with it. Unfortunately, plain WebGL API is rather too low level. By working in a more business-oriented way than research-oriented way, we considered to limit the solution to a higher level – deck.gl. This way we were able to create a working prototype and see the results much earlier.
In general, it’s good practice to cover production code with tests, but what’s even more important is that tools used to write tests also work as intended.
Our main assumptions were:
- The deck.gl library and its related ecosystem are already tested and work 100% correct.
- If deck.gl receives same input, it will produce same output.
Having established the baseline, we look at the higher level – an interaction between the app and the deck.gl API. The solution relies on public API offered by deck.gl and uses a bit of internal API of deck.gl to retrieve the layer hierarchy. Now, if we inject a piece of code in the middle of deck.gl and application, we should be able to observe how the application is manipulating deck.gl’s input and retrieving deck.gl state to check all objects’ properties. Practically, we can achieve this by injecting props, intercepting deck instance, building all necessary internal structures for faster object finding, and finally export utility functions to operate the system. Those exported functions are the core value of our plugin, an API which any QA engineer can use in their automated tests.
Our work towards getting this up and running began by writing the “backend” part, which meant providing a plugin inside the source code that would function as a middleware between our custom app and deck.gl itself. It’s supposed to receive the app’s required properties, process them by setting up proxy event handlers, and return new props that eventually get forwarded to Deck.gl.
That part was achieved by providing Test IDs to all layers we render on the map. If you’ve ever debugged deck.gl layers, you’ll know that there’s no such property as testId, but if you forward a custom TypeScript interface to the deck.gl layer with that new string property, you’ll be able to write getters and setters without problems.
After this was ready, we had to think of a way to export something to the browser’s window object so that Cypress can access the rendered data. Therefore, we created an instance of our custom DeckInspector class, which we’re exporting once the map renders with its data.
From that point on, Cypress can access the class functions by calling window.deckInspector, and the rest is up to the testers. Some of our successfully implemented and tested functions provided by our DeckInspector class are getElementByTestId(testId), zoomTo(element), getText(element), and getLayerHierarchy(), which fetches the complete hierarchy of all layers visible on the map. Once the plugin is ready, functions for fetching specific layer features will also become available, (such as dglFillColor or dglCenter).
Despite this being our initial proof-of-concept version, we haven’t noticed a lot of unfixable open points. The two biggest issues for us were dragging and dropping elements onto the map / canvas, as well as fetching the coordinates of a rendered layer’s center point. Tooltips, which are also an open point, have not yet been covered or inspected in terms of whether they are testable.
Last, but not least, once the plugin becomes publicly available, test coverage data of the plugin itself should also be provided.
We’ve only scratched the surface, and yet we’re very optimistic about the future of this approach, as well as how much potential it offers now, despite its limitations! We hope you enjoyed reading this as much as we enjoyed researching and writing about Cypress and deck.gl!