Evolving Browsersync with the 200th Release

This latest release, our 200th, was focussed around improving the client-side part of the application.

Firstly, what is Browsersync?

JH created tool Browsersync is a free open source project designed to synchronise file changes and interactions across multiple devices, we’re hugely proud of its success (around 100k downloads a month), and grateful for its existence on a daily basis!

Put simply, Browsersync reduces testing time. And never has this been more important than today, when the number of devices that we deliver web applications on is more than ever: testing has become an increasingly repetitive task. Browsersync works across desktop and mobile devices at the same time. It updates code changes, synchronizes scroll positions and form inputs – all automatically across browsers.

It’s a free open source tool, that’s consistently ranked first by the Developer community when it comes to browser live-update / synchronization tools. Its industry-leading reputation is demonstrated in it being chosen by the likes of Google and Adobe. It’s easily integrated with your web platform, build tools and other Node.js projects.

Highlight from this release: the client-side aspect has been rebuilt to offer new features and an improved workflow

The Browsersync Client is the small bundle of code that is automatically injected into your website at runtime. It allows the Browsersync server to communicate with all connected devices and also provides a way for devices to ‘broadcast’ messages to each other (this is how scroll-syncing works, for example).

Whilst it’s a crucial part of the technology (Browsersync wouldn’t be very useful without it) it’s also highly decoupled from the server-side parts of the system. This is a more a natural consequence of being a message-passing system rather than a deliberate technical decision – but either way, it’s a great situation to be in!

Having a decoupled message-passing system like this allows developers the freedom to improve individual pieces without affecting others. That means something that would normally seem quite extreme in other systems (like a ‘complete rewrite’ for example) becomes as simple as ensuring the incoming and outgoing messages match up.

With this freedom in mind, we set about improving a few key areas related to workflow, features and testing of the Browsersync Client.

Typescript

The bulk of the logic in the Browsersync client is related to the sending and receiving of messages. That means we’re relying on raw JavaScript Object Literals and Arrays everywhere, for everything. The ‘shape’ of these Objects and Arrays then is something we’d like to verify for outgoing messages. We can do this easily with Typescript – and once we have type safety for outgoing messages, we can also use that information to enforce the correct usage of incoming messages.

Providing type safety for messages is only one part of the story, however. Since the Browsersync client operates inside actual browsers, it means that Typescript can guide and enforce the correct usage of all DOM properties, events and any other APIs native to the Web.

Finally, since Typescript can emit JavaScript code for a number down-level targets, it means we can author in the latest and greatest version of the language and have Typescript produce valid ECMAScript 5 (all without needing a separate tool, such as Babel).

RxJS

RxJS is all about making it easier to compose asynchronous or callback-based code – basically, it’s a swiss-army knife for event-based programs. This makes it a perfect fit for the Browsersync client, as the entire application is about nothing more than orchestrating events. Whether it’s messages from server to clients over Web Sockets, click/scroll/form events inside each browser or even the internal communication between modules – events are the cornerstone of the application.

RxJS is such a natural fit then – just using it to model the interactions between all the moving pieces was immediately useful on its own. But once we started the rewrite, we soon realised that the compositional features provided by RxJS would allow us to solve problems in simpler ways.

One stand-out example of where the switch to RxJS made things ‘simpler’ in the end, is the way in which scroll events are broadcast and replayed. When you scroll on any device, the element and its scroll position are broadcast to all other devices – that part is simple enough, but when it comes to replaying that scroll position on the other devices, you need to temporarily disable scroll event *listeners* so that you don’t fall into an infinite loop.

This was previously achieved with logic on both the client and the server to ensure only one device was ever deemed the ‘controller’. Moving to RxJS though, we were able to solve this much more elegantly by blocking scroll listener events from being broadcast at all, if they they follow an incoming event. This was all made possible since we now model *everything* as ‘streams’ internally, and we can use a windowed buffer from the incoming stream to temporarily ‘block’ the messages that would cause the infinite loop.

That’s just one example, but this theme of things getting ‘easier’ was observed throughout the rewrite.

Integration testing

The final part of this rewrite involved new integration tests using Cypress.io

We have a huge number of users and we take changes very seriously. For a long, long time we’ve been experimenting with ways to test the entire E2E experience. To do so would allow us to iterate on the entire project in a more confident way – after all, we don’t want to break a tool that thousands of people use daily in their work!

Unit tests have their place for sure (we have hundreds), but Browsersync is a perfect example of a project where you could have 1,000 unit tests, 100% code coverage and all the Travis CI badges you like, but it could still break when all the pieces attempt to run together!

What the Browsersync project needed was a way to test a fully-working, realistic use-case. An example would be starting Browsersync with a given configuration, navigating to a page, changing a CSS file, and then verifying that the CSS was actually injected.

This is where Cypress came to the rescue. We’ve tried Selenium, many times. We always found it to be too slow and flaky for our needs – you could never guarantee the tests would pass, even though you knew, 100% that everything was working.

With Cypress though, these problems vanished. It also gave us access to the full test environment (so we can see console.log messages during development), the ability to query the DOM directly & most importantly of all it can even execute system commands!

This was a turning point. Since it can execute system commands we were able to achieve the perfect integration test setup by using regular commands to do things such as trigger a file-change event. Following this file-change event, we can then reach into the DOM to ensure the file in question was successfully updated. Cypress made these types of tests a breeze, and whilst we only have a handful right now, we’ll be adding many more in the weeks to come.Now we can model real-world use cases far more accurately.

You can check out the release notes if you’re interested in the code.