Ashley Sheridan​.co.uk

Testing Popstate Events with Jasmine

Posted on

I recently implemented some browser History API functionality in a project, and part of this was using popstate events to handle application state when the user navigated with their browsers navigational buttons.

The problem was when it came time to write tests to cover this functionality, because I was writing in TypeScript, which is a strictly type language, and the version of TypeScript I was using didn't have versions of Event or Custom event that had state properties.

This is the code I'm testing:

bindPopStateEventCallback() { this._window.addEventListener('popstate', this.popstateChange.bind(this)); } popstateChange(event: any) { // do your custom thing here }

Method 1

Initially, I thought to just try adding the state property to the CustomEvent object in the normal manner within my test:

it('should trigger a popstate event when the back button is clicked', () => { let event = new CustomEvent('popstate'); event.state = {foo: "bar"}; spyOn(window, 'addEventListener').and.callThrough(); spyOn(yourCustomObject, 'popstateChange'); yourCustomObject.bindPopStateEventCallback(); window.dispatchEvent(event); expect(yourCustomObject.popstateChange).toHaveBeenCalledWith(event); });

This didn't work, because TypeScript (at least the version I was using) didn't have a CustomEvent that contained a state property, and being strictly typed, it won't let you put custom properties on in the code that it can compile before running.

Method 2

The next obvious choice was to create my own custom CustomEvent and put a state property on that instead.

In the project I was working on this involved adding over a dozen custom properties, adding extra events, etc. This was because of the version of TypeScript I was using.

However, in more recent versions of TypeScript, it's a lot more simple:

class CustomPopstateEvent extends CustomEvent { public state: any; constructor(state: any) { super('popstate'); this.state = state; } }

Method 3

This was the most hacky of all the approaches, and requires some trickery to get TypeScript to accept it:

let event = new CustomEvent('popstate'); (event).state = {foo: "bar"};

This casts back the CustomEvent to the any type (which is not really a type but all types), and that allows you to add in any custom property you want. It doesn't actually convert the object, just casts it for the purposes of that line of code. It's not perfect, and is a method that is prone to awful abuse. However, given that it's only being used in this test and not code that is run by a user in production, I feel it's an acceptable compromise.

Method 4

I've saved the best until last:

let event = new PopStateEvent('popstate'); event.state = {foo: "bar"};

That's it, the perfect solution, and it's that simple.

Conclusion

If you're in a situation where you need to test various types of custom events, then it's most likely that you'll be able to create the correct types of test events. In the cases where you can't directly create the type of object that your events require, then you should be able to mock them using an extended CustomEvent.

For the rare situations where neither of the best options are available, then there is the dirty third method using temporay coercion. However, I would strongly recommend against using something this in the production code run by your users. If you're using a strongly typed language like TypeScript, then your code should be TypeScript and not rely on weird workarounds in order to be more like regular JavaScript. Avoid using things like this in your production code if you can, there's really not a compelling reason to put it there. If it's just in your tests though then you have a little room for creativity.