WebExtension APIs made as getter/setter, lazily evaluating to functions – Beware of mocking for unit tests!


(rugk) #1

I was writing unit tests with sinon and wanted to mock some WebExtension APIs, in my case the browser.i18n.getMessage.
I used the mock API of sinon, so I e.g. wanted to mock it like this:

const mockI18n = sinon.mock(browser.i18n);
mockI18n.expects("getMessage")
  .once().withArgs("extensionNameShort")
  .returns("My fake extension name 101");

However, it always failed with an error like this:

TypeError: Attempted to wrap undefined property getMessage as functioncheckWrappedMethod@https://unpkg.com/sinon@6.1.4/pkg/sinon.js:3910:21
wrapMethod@https://unpkg.com/sinon@6.1.4/pkg/sinon.js:3959:13
expects@https://unpkg.com/sinon@6.1.4/pkg/sinon.js:2014:13
@moz-extension://3bfb81c2-4710-453d-ba2a-bda1094a1fd9/tests/localiser.test.js:44:13

So i finally found out that the whole browser.i18n API is just a collection of properties with getters (and setters) and only if you call them once – or, even if you just do this, you can circumvent the “problem”:

// just access once, do nothing with result

/* eslint-disable no-unused-expressions */ /* (if you use this) */
browser.i18n.getMessage;
/* eslint-enable no-unused-expressions */

Now, this code is of course somewhat awkward, as it literally just accesses a property and does nothing with it, but Firefox then seems to replace the browser.i18n.getMessage with an actual property, which can be mocked/doubled as usual with tools like sinon.


I just wonder why Firefox does this? (I did a quick test with Chromium, and it did not do the same, it always seems to have functions there. At least the console reports this.)

Also, IMHO, it is kinda bad this is not documented anywhere. Especially as it seems to affect all WebExtension APIs of Firefox. You can just look into the console and type the API and you should see there are just Getter & Setter by default. See e.g. browser.tabs:
browser.tabs{…}MutedInfo: Getter & SetterMutedInfoReason: Getter & SetterPageSettings: Getter & SetterSharingState: Getter & SetterTAB_ID_NONE: Getter & SetterTab: Getter & Setter

When you then access query, e.g. you then see it is “evaluated” to a function:
printPreview: Getter & Setterquery: function ()reload: Getter & Setter


(Martin Giger) #2

I think this is to lazily instantiate APIs to avoid loading their code at all when they’re not used.


(Niklas Gollenstede) #3

I’m pretty sure Edge does this as well.

It’s nowhere documented that they are configurable, enumerable, writable own value properties either, so relying on any of that is relying on undocumented implementation details. (It’s also not documented that the functions don’t rely on their this.)