Unable to mock event handler on test - javascript

So i have a very simple class which does the following:
Adds an event handler on an element on it's creation
The event handler is an instance method of the class
I need a cleanup method to remove the handler on demand
export default class TestClass {
constructor() {
this.element = document.getElementById("test");
this.element.addEventListener("click", this.clickHandler);
}
clickHandler(e) {
console.log("Click handler called");
}
cleanup() {
this.element.removeEventListener("click", this.clickHandler);
}
}
And i have a test where i stub the handler (i need to change it's behavior) and make some checks.
describe("Test", function () {
beforeEach(function () {
this.instance = new TestClass();
this.clickHandlerStub = sinon.stub(this.instance, "clickHandler");
});
afterEach(function () {
this.instance.cleanup();
delete this.instance;
});
it("test case", function () {
document.getElementById("test").click();
expect(this.clickHandlerStub.calledOnce).to.equal(true);
});
});
The expected behavior is to stub (override) the handler (so no logging should appear) and my expectation should pass since it should be called once when the element is clicked.
However it seems that it keeps logging and the expectation fails.
If i change the way i bind the event handler and use an arrow function everything works fine.
this.element.addEventListener("click", () => this.clickHandler());
But then i can't remove the event handler in my cleanup method since addEventListener and removeEventListener need to pass the same handler reference in order to work properly.
I created a reproducable example in codesandbox in case it's helpful.
So what am i missing here?

You can stub TestClass's prototype.
this.clickHandlerStub = sinon
.stub(TestClass.prototype, "clickHandler")
.callsFake(() => {
console.log(`Now it's calling the fake handler`);
});
The cleanup would be
this.clickHandlerStub.restore();
https://codesandbox.io/s/mocha-unit-test-example-forked-sstfz?file=/src/index.js
References:
Restore sinon stub
The idea of stubing prototype
By the way, I prefer not to use this in global context. Instead, I'd create them as let variables.
describe('...', () => {
let clickHandlerStub;
beforeEach(() => {
clickHandlerStub = ...
});
afterEach(() => {
clickHandlerStub.restore();
});
});

Related

can't bind this to function

I have a DOM element btn and have function with setTimeout which listens on this element.
I want to remove my listener while function is running to prevent multiple calls of setTimeout and time overlapping, but this in my callback function is window? I tried to use bind but it's not working
const checkTable = () => {
this.removeEventListener('click', checkTable);
console.log(this); // i get "window"
setTimeout(() => {
//some code
}, 3000);
};
const app = () => {
const checkBtn = document.querySelector('.check-btn');
checkBtn.addEventListener('click', checkTable.bind(checlBtn))
};
app();
and the same result with onclick event
You can't call .bind() on an arrow function. Arrow functions are used to automatically use the this value from where it's defined. For this to work, your checkTable needs to be a "normal" function.
function checkTable() {
this.removeEventListener('click', checkTable);
console.log(this); // i get "window"
setTimeout(() => {
//some code
}, 3000);
}

How to create a javascript functional 'class' so that I can access a 'method' from outside and inside the function

I am creating a function that handles a bunch of stuff around pagenating and sorting a table. It contains a key function that submits the db query and updates the display table.
I want to be able to access that inner function/method from both inside the function and also from outside on the object created.
testFunction = function() {
keyMethod = function() {
console.log('ya got me');
};
document.getElementById('test').addEventListener('click', function (e) {
keyMethod();
});
keyMethod();
};
myTest = new testFunction();
myTest.keyMethod();
testFunction = function() {
this.keyMethod = function() {
console.log('ya got me');
};
document.getElementById('test').addEventListener('click', function (e) {
// would have to use bind here which then messes up trying to
// find the correct target etc.
keyMethod();
});
this.keyMethod();
};
myTest= new DrawShape();
myTest.keyMethod();
Creating it the first way means that the keyMethod function is available everywhere within the testFunction but I cant call it from outside.
Creating it the second way means I can do myTest.keyMethod but I then cant call it from within an inner function without using bind everywhere.
Is there a better way..?
You could replace the function provided as callback with an arrow function or use bind the function first like you already said.
testFunction = function() {
this.keyMethod = function() {
console.log('ya got me');
};
// Replace callback by simply providing the function to call.
// This works as long as you don't use the `this` keyword inside the
// provided function.
document.getElementById('test').addEventListener('click', this.keyMethod);
// If your callback method does use the `this` keyword you can either use an
// arrow function or bind the function up front.
document.getElementById('test').addEventListener('click', event => this.keyMethod());
document.getElementById('test').addEventListener('click', this.keyMethod.bind(this));
this.keyMethod();
};
console.log("constructor output:");
myTest = new testFunction();
console.log(".keyMethod() output:");
myTest.keyMethod();
console.log("click event output:");
<button id="test">test</button>

Reusable event listener callback within class

I write code in pure JS and I need to have reusable callback for my event listener within the class. It is required to meet following:
reusable by another functions
event listener needs to be revocable by another functions
I need to pass argument (event)
I need to be possible to call another function from the callback (this.doSomething())
I tried define callback as method and also as function expression but every time I solve one issue another occurred. I have walked through many questions here too but still can not make my code to work.
class Foo {
constructor() {
functionA()
this.howAndWhereToDefineThisCallback = function(event) {
event.preventDefault();
this.doSomething();
}
}
functionA() {
let el = document.getElementById('element1');
el.addEventListener( 'click', howAndWhereDefineThisCallback );
this.functionB();
}
functionB() {
let el = document.getElementById('element1');
el.removeEventListener( 'click', howAndWhereToDefineThisCallback );
}
doSomething() {
// something meaningful
}
}
How can I modify my code to use it the way I just described?
Here you have an implementation:
// Callback defined outside the class.
function callback(event) {
this.doSomething();
}
class Foo {
constructor(cb) {
// Important: You have to bind it.
this.cb = cb.bind(this);
this.functionA();
}
functionA() {
let el = document.getElementById('element1');
el.addEventListener('click', this.cb);
}
functionB() {
let el = document.getElementById('element1');
el.removeEventListener('click', this.cb);
}
doSomething() {
console.log('doing something...');
}
}
const foo = new Foo(callback);
// foo.functionB();
<button id="element1">
Click here
</button>
If you want to reuse your callback function, simply put it outside of your class scope. In order to call another function from your callback, just put that function as a argument of your callback, for example:
var howAndWhereToDefineThisCallback = function(event, anotherCallback) {
event.preventDefault();
if (anotherCallback) anotherCallback();
}
To use the callback in your class method:
el.removeEventListener( 'click', function(event) {
howAndWhereToDefineThisCallback(event, this.doSomething);
});

How can I test for equality to a bound function when unit testing?

I want to test that an argument passed to a function is a function reference but the function reference is being passed using bind().
Consider this code which is to be tested (shortened for brevity):
initialize: function () {
this.register(this.handler.bind(this));
}
And this unit test to check if register() was called with handler():
it('register handler', function () {
spyOn(bar, 'register');
bar.initialize();
expect(bar.register.calls.argsFor(0)[0]).toEqual(bar.handler);
});
The arg doesn't equal the function reference I guess due to the bound function using bind() - how can I test that the correct function reference is being passed while still using the bind() method on it?
Note: This isn't specific to jasmine, I just thought it was appropriate because of the methods being used.
Instead of
expect(bar.register.calls.argsFor(0)[0]).toEqual(bar.handler);
you can do
expect(Object.create(bar.handler.prototype) instanceof bar.register.calls.argsFor(0)[0])
.toBe(true);
or
expect(Object.create(bar.handler.prototype)).
toEqual(jasmine.any(bar.register.calls.argsFor(0)[0]));
This works because the internal [[HasInstance]] method of the bound function delegates to the [[HasInstance]] method of the original function.
This blog post has a more detailed analysis of bound functions.
this.handler.bind(this) creates completely a new function, therefore it is not equal to bar.handler.
See Function.prototype.bind().
You can pass bounded function as argument to your initialize function and then test it, e.g.:
var handler = bar.handler.bind(bar);
bar.initialize(handler);
expect(bar.register.calls.argsFor(0)[0]).toEqual(handler);
I've managed to keep the test and code and work around it.
I spy on the function reference with an empty anon func, then call it when spying on the register method - if the spy gets called, I know it's passed the correct reference.
it('register handler', function () {
spyOn(bar, 'handler').and.callFake(function(){}); // do nothing
spyOn(bar, 'register').and.callFake(function(fn){
fn();
expect(bar.handler).toHaveBeenCalled();
});
bar.initialize();
});
I thought I'd add another approach that, to me, is a bit less awkward.
given a class like:
class Bar {
public initialize() {
this.register(this.handler.bind(this));
}
private register(callback) {}
private handler() {}
}
the full spec might look like:
describe('Bar', () => {
let bar;
beforeEach(() => {
bar = new Bar();
});
describe('initialize', () => {
let handlerContext;
beforeEach(() => {
bar.handler = function() {
handlerContext = this;
};
bar.register = jest.fn(callback => {
callback();
});
bar.initialize();
});
it('calls register with the handler', () => {
expect(bar.register).toHaveBeenCalledWith(expect.any(Function));
});
it('handler is context bound', () => {
expect(handlerContext).toEqual(bar);
});
});
});
In my case (using jest) I just mocked the implementation of bind for the function I wanted and I tweaked it so that it returns the original function and not a bound copy of it.
Specifically here's what I tried and worked:
Code to be tested:
// module test.js
export const funcsToExecute = [];
function foo(func) {
funcsToExecute.push(func);
}
export function bar(someArg) {
// bar body
}
export function run(someArg) {
foo(bar.bind(null, someArg));
}
I wanted to assert that when run is called, funcsToExecute contains bar
So I wrote the test like this:
import * as test from 'test';
it('should check that "funcsToExecute" contain only "bar"', () => {
jest.spyOn(test.bar, 'bind').mockImplementation((thisVal, ...args) => test.bar);
test.run(5);
expect(test.funcsToExecute.length).toBe(1);
expect(test.funcsToExecute[0]).toBe(test.bar);
});
For your example, I suppose it would be something like this:
it('register handler', function () {
spyOn(bar, 'register');
spyOn(bar.handler, 'bind').mockImplementation((thisVal, ...args) => bar.handler);
bar.initialize();
expect(bar.register.calls.argsFor(0)[0]).toBe(bar.handler);
});
though I haven't tested it.

How to use Jasmine spies on an object created inside another method?

Given the following code snippet, how would you create a Jasmine spyOn test to confirm that doSomething gets called when you run MyFunction?
function MyFunction() {
var foo = new MyCoolObject();
foo.doSomething();
};
Here's what my test looks like. Unfortunately, I get an error when the spyOn call is evaluated:
describe("MyFunction", function () {
it("calls doSomething", function () {
spyOn(MyCoolObject, "doSomething");
MyFunction();
expect(MyCoolObject.doSomething).toHaveBeenCalled();
});
});
Jasmine doesn't appear to recognize the doSomething method at that point. Any suggestions?
Alternatively, as Gregg hinted, we could work with 'prototype'. That is, instead of spying on MyCoolObject directly, we can spy on MyCoolObject.prototype.
describe("MyFunction", function () {
it("calls doSomething", function () {
spyOn(MyCoolObject.prototype, "doSomething");
MyFunction();
expect(MyCoolObject.prototype.doSomething).toHaveBeenCalled();
});
});
When you call new MyCoolObject() you invoke the MyCoolObject function and get a new object with the related prototype. This means that when you spyOn(MyCoolObject, "doSomething") you're not setting up a spy on the object returned by the new call, but on a possible doSomething function on the MyCoolObject function itself.
You should be able to do something like:
it("calls doSomething", function() {
var originalConstructor = MyCoolObject,
spiedObj;
spyOn(window, 'MyCoolObject').and.callFake(function() {
spiedObj = new originalConstructor();
spyOn(spiedObj, 'doSomething');
return spiedObj;
});
MyFunction();
expect(spiedObj.doSomething).toHaveBeenCalled();
});

Categories

Resources