Bind ES6 arrow functions to Context
michielbdejong opened this issue ยท 11 comments
disclaimer: I'm quite new to Mocha and to arrow functions, so feel free to close this if it's nonsense. :)
Consider these three tests:
test('Mocha Context available in normal ES5 functions', function() {
expect(this.slow).to.be.a('function');
});
test('Mocha Context available ES6 arrow functions', () => {
// Will fail, because it's bound to `window`:
expect(this.slow).to.be.a('function');
});
test('Mocha Context available in nested, bound ES6 arrow functions', function() {
(() => {
expect(this.slow).to.be.a('function');
})();
});
afaics this is because Mocha uses fn.call(ctx);
to execute the function passed to test
, which does not have the desired effect in case of ES6 arrow functions, as you can read here and also see if you run this in your browser console:
(() => { console.log(this); }).call({ foo: 'bar' });
// prints the Window object
(function() { console.log(this); }).call({ foo: 'bar' });
// prints { foo: 'bar' }
(function() { (() => { console.log(this); })(); }).call({ foo: 'bar' });
// prints { foo: 'bar' }
Looking especially at how that third test (arrow function wrapped inside a normal function) does pass, I think it could be fixed by wrapping the variable fn
in a normal ES5 function before calling its .call
method, i.e. replacing that fn.call(ctx);
with (function() { fn(); }).call(ctx);
on line 289 of Runnable.js
Or the easier option would of course be to add a note to the documentation saying: Don't put your test in an arrow function if you plan to use this.slow
or other methods from the Context. :)
@michielbdejong The documentation suggestion works, or Mocha could pass the test context as an argument in addition to setting it as this
. Also, it's not possible to change the value of this
in an arrow function, since they are lexically scoped. If you look at the Babel's output you'll see this is the case:
@michielbdejong Your suggested fix doesn't seem to work in practice
var assert = require('assert');
function it(str, fn) {
var ctx = {
skip: function() {}
};
(function() {
fn().call(ctx)
}).call(ctx);
}
it('Mocha Context available ES6 arrow functions', () => {
assert.equal(typeof this.skip, 'function');
});
$ node --version
v4.0.0
$ node example.js
assert.js:89
throw new assert.AssertionError({
^
AssertionError: 'undefined' == 'function'
The reason that (function() { (() => { console.log(this); })(); }).call({ foo: 'bar' });
works is because the inner arrow function is already bound to its parent function's context after the call()
How babel transpiles the codes should make it evident why this won't work:
(function() {
it('test', () => {
console.log(this);
});
});
// =>
'use strict';
(function () {
var _this = this;
it('test', function () {
console.log(_this);
});
});
No matter how you bind/call the function passed to it
, it still has a reference to the parent's context.
Closing for now. An issue/pr in https://github.com/mochajs/mochajs.github.io would be appreciated! :)
Your suggested fix doesn't seem to work in practice
Oh, of course, sorry. I see why now. Thanks for taking the time to write a code example!
I created mochajs/old-mochajs-site#14.
No problem - my commit referenced in this issue should hint that I thought it would work too haha.
And appreciate the PR! :)
If anyone finds this solution unsatisfactory, as I did, because you'd really prefer to use arrow functions, I have good news! You can just declare the top level describe
with a normal function: e.g. describe('MyClass', function () {
It works because any arrow functions within will have this
bound to their containing function's scope (i.e. the mocha context). So the follow will totally work...
describe('thing', function () {
describe('does stuff', () => {
beforeEach(() => {
this.foo = 'bar'
})
it('should work', () => {
assert(this.foo === 'bar')
})
})
})
๐๐๐
...๐ ๐คท
WARNING: that is not quite the same thing* and might not behave equivalent to the non-arrow-function code for:
- nested suites
- configuration (
this.timeout(<milliseconds>)
and the like) currentTest
in hooks- anything else
this
-related that would normally affect the specifically the current hook or test or otherwise be specially handled
* For better or for worse, Mocha is doing some magic regarding the this
object to achieve all the uses it supports, so it isn't simply sharing the same context object between all suites and tests.
@ScottFreeCode thanks for the tip! I've edited my comment to reflect my reduced confidence.
Is there an option to pass a custom ctx
so that it
callbacks are bound to it?
We prefer binding our tests to a certain "library" as this
.
My current workaround is:
const lib = global._lib;
before(async function() {
// Any properties can be set on `this` for use in tests.
// https://dev.to/open-wc/shared-behaviors-best-practices-with-mocha-519d
//
// There seems to be no better way of turning `this` into `lib` in tests.
// https://github.com/mochajs/mocha/issues/1856#issuecomment-854206335
//
for (const key in lib) {
this[key] = lib[key];
}
});