acvetkov / sinon-chrome

Testing chrome extensions with Node.js
ISC License
434 stars 46 forks source link

Please document how to use browser / webextension API in tests #68

Open arantius opened 6 years ago

arantius commented 6 years ago

Here's a reduced test case: https://gist.github.com/arantius/33d318f57ced2f1b7ef5c49740b00e01

Try to use sinon-chrome, with browser API. Fail. chrome here works (well, doesn't crash before Karma initialization does, for this reduced case). Documentation claims both APIs are supported, but I can't tell how.

Sxderp commented 6 years ago

I've been looking into this, and this is what I've found.

Firstly, loading sinon-chrome as a framework requires the karma-sinon-chrome package. However, this package requires an older version of sinon-chrome and therefore downloads and uses that version (in it's own local node_modules namespace).

Unfortunately, that's only part of the problem. Suppose you manually load the WebExtension part of sinon-chrome in the files section of the karma.conf[1] you will only have access to a global 'chrome' object. Despite the name, this is the actual 'browser' API[2]. The reason that that the global object name is 'chrome' is that the auto-bundler (webpack) is configured to name the object 'chrome'.

What makes this even worse is that, while the APIs are 'defined', the stubs that are created are not configured to return promises. Therefore any time you perform a .then the interpreter will error with browser.func() is undefined (since nothing is returned).

Although, creating the stubs to autoresolve a promise would probably create all sorts of problems too. Since the promise chain would be expecting particular input and I suppose it can only be simulated to a certain extent.

Along those lines, the best thing I could offer is to assume, during testing, that all browser functions are working properly (heh) and use before() blocks to configure what the functions resolve to.

[1] With say, ./node_modules/sinon-chrome/bundle/sinon-chrome-webextensions.min.js. [2] As defined in https://github.com/acvetkov/sinon-chrome/blob/058cc9304fedfb3db0e04eae7ff0bdbe93dfdda2/src/config/stable-api-ff.json


It should also be noted that the Karma config linked will uses the actual browser to execute the tests. This particular package was created to run tests in a node environment. Thus the expectation would be to do the following:

const chrome = require('sinon-chrome/extensions');
const browser = require('sinon-chrome/webextensions');

You could use something like requireJS to mimic this functionality. I attempted to do this with the Greasemonkey codebase, unfortunately due to async loading (of requireJS) and the fact that the GM codebase isn't based on a module / export structure I was unable get it working properly.

acvetkov commented 6 years ago

@arantius hi. your problem is related to karma-sinon-chrome, not sinon-chrome. karma-sinon-chrome uses old dependency.

@Sxderp you are great!

arantius commented 6 years ago

Thanks for the confirmation of that. I'm working on a PR ( https://github.com/9joneg/karma-sinon-chrome/pull/5 ) with them to get their internal dependency updated.

So I've updated the originally linked example, to use my updated branch. I still get the same ReferenceError: browser is not defined. Does the above ("... the global object name is 'chrome' is that the auto-bundler (webpack) is configured to name the object 'chrome'.") imply that I must have set up code to alias chrome to browser to make this work?

Sxderp commented 6 years ago

Does the above ... imply that I must have set up code to alias chrome to browser to make this work?

Just to be completely clear I want to take a step back. Some of the following you might already know.

Node packages are created in such a way that all public facing APIs must be assigned to module.exports where module is a global reference to the current 'package' that Node handles itself. This package, sinon-chrome uses the native export default syntax. Which is slightly different but for our purposes it can be the same. Therefore, in this package, the following line in src/extensions/index.js is functionally equivalent to the second.

export default new Api(config).create();
module.exports = new Api(config).create();

Now, in order to use the module, in your main code (in a Node environment) you would do:

const chrome = require('sinon-chrome/extensions');

Which is basically the same as (not valid code, don't do the below).

const chrome = module.exports = new Api(config).create();

In order to provide this package in a non-Node environment a script (webpack) is run. What this does is bundle all the necessary files into a single file. This can be represented with a single line of code (not valid code, but demonstrates what's happening).

// For src/extensions
window.chrome = require('sinon-chrome/extensions')

Furthermore, the bundler does the following when it creates the bundled src/webextensions module.

window.chrome = require('sinon-chrome/webextensions')

So yes, if you include the WebExtension bundled file sinon-chrome-webextensions.min.js you need to create an alias from chrome to browser. And this shouldn't cause any problems since the FF ecosystem uses the same endpoint names for both namespaces (as far as I'm aware).


Looking at the patch you submitted, the update would only give access to the endpoints defined for Chrome extensions: here. Since Chrome and FF use slightly different endpoints aliasing chrome to browser would not be a solution, assuming you continue using karma-sinon-chrome.

The simplest solution is to just include the the endpoints you want, in this case FF, in your list of Karma files and then alias chrome to browser.

Sxderp commented 6 years ago

Alternatively, although much more work you could modify the code your testing (in this case Greasemonkey) to use a module.exports structure with either requireJS or natively (I don't think FF supports this). Then you would be able to test your code directly in Node without having to serve the files like Karma does.

Probably not the best idea.

stoically commented 6 years ago

One way to test completely in Node without exporting the system under test using module.exports is to assign browser on global, then require the production code and finally wait until process.nextTick. Here's an example how to do so.

Another way would be to have something like if (process && module && module.exports) { module.exports = foo; } in the system under test which would allow to export/require the actual code without having to use e.g. requireJS. Or setting global._testEnv = true in the test and doing if (_testEnv) { module.exports = foo; } in the production code. Admittedly those are not the cleanest solutions, but personally I think it's a small price to pay for easy to test code.