2015-08-31

meteor + sanjo:jasmine + urigo:angular + mocks: `ReferenceError: module not defined` in Client-Integration tests

Till now, I've been trying to get client-unit-test mode running and refreshing properly. So far, it all seems running just fine. Next point on my list was adding urigo:angular and angular:angular-mocks (1.4.2) to see how it works with Meteor and testing.

Setting up a very simple single-file Angular app went fine. I've added some trivial client-unit tests and everything was working just great. Angular-mocks were doing their job well, too. App was refreshing, tests were updated properly. Optimistically, I clicked 'add sample integrations tests'. Finished, ran, green, all great.

However, the sample tests are self-contained. All code is dropped to somewhere in /tests directory, nothing really touches the actual application, and of course the auto-generated sample tests have nothing to do with angular. So, I copied/edited some tests from the client-unit mode and ... boom:

ReferenceError: module is not defined

at:

beforeEach(module('mytestapp'));

It's possible that not only I am seeing such issues. Problem seemed quite obvious: In client-integration, AngularJS (or rather, the mocks) were not loaded or at least not available. Quite strange since everything worked fine in client-unit mode. However, I checked configs, packages, and even the emitted source of the test runner, and everything indicated that the angular-mocks are provided. There had to be some other cause.

AngularMocks expect Jasmine to be already present

AngularMocks 1.4.2 at line 2190 expects window.jasmine or window.mocha to be already defined. Typically, it is set by jasmine-core, but in Meteor, it is the sanjo:jasmine package that ensures that window.jasmine is properly set. Both the expectation and the provision are performed at package-load time.

For all meteor packages, load order are determined by the interpackage dependencies. Since angular:angular-mocks can be used with either Mocha or Jasmine (or probably many other frameworks too), it so it can't/shouldn't strictly depend on either of them. (It seems someone wouldn't want to have angular-mocks-jasmine and angular-mocks-mocha which would probably be the most reasonable way, but, anyways..).

Solution #1: add proper optional dependecy to angular-mocks package - failed

Fortunatelly, Meteor provides a concept of weak dependency, i.e.:

api.use('sanjo:jasmine', ['client'], {weak: true});

which ensures that such package is loaded before the current one - but it is not pulled until something else requires it strongly (weak:false or no specifier at all). That's just perfect here, I wondered, why wasn't that working?

I was suprised to see that despite adding angular:angular-mocks by meteor add, the only actual Meteor packages here are:

  • urigo:angular
  • sanjo:jasmine

It turns out that you will not find angular:angular-mocks Meteor package on GitHub, nor in the Meteor core set (well, obvious). It simply is not a Meteor package. I checked packages.meteor.com package directory and it turned out to be ... a Bower package.

..and, as you may see in its bower.json, the only dependency is Angular and there are no weak dependencies. I searched a little and it turns out that Bower has no such concept as "weak/optional dependency".

Too bad, I though, but maybe there is some option for Meteor package to indicate a "reverse weak dependency", that is, to indicate not what packages must be loaded before mine, but rather, what packages my package has to be loaded before. Having such option set on sanjo:jasmine could mark this package as to-be-loaded-before angular:angular-mocks.

Unfortunatelly, this has turned out to be not possible either because Meteor does not support it now, and it doesn't seem planned either. Moreover such feature was requested earlier and seems to have died. Maybe it's worth resurrecting that topic..

Solution #2: wrap angular-mocks into meteor package - ok!

Since Bower does not support optional dependencies and Meteor does, that's pretty obvious attempt. I tried to provide my own 'angular-mocks' with that weak dependency specified:

Package.onUse(function(api) {
  api.versionsFrom('1.1.0.3');
  api.addFiles('client/angular-mocks.js', 'client');

  api.use('sanjo:jasmine', ['client'], {weak: true});
  api.use('urigo:angular', ['client'], {weak: true});
});
meteor remove angular:angular-mocks
meteor add quetzalcoatl:sanjojasmine-angularmocks

It works! However in the long run it seems pointless. I don't want to create and mantain and update such clone just to correctly specify the deps. In case you have such problem and other solutions don't work for you, you can at least temporarily solve it like that.

Solution #3: add dependencies in proper order - ok!

Just for fun, I checked if the order of adding packages to meteor app makes any difference:

meteor add urigo:angular my-mocks sanjo:jasmine                                  # works
meteor add my-mocks urigo:angular sanjo:jasmine                                  # works
meteor add urigo:angular sanjo:jasmine my-mocks                                  # works
meteor add urigo:angular angular:angular-mocks sanjo:jasmine                     # broken
meteor add urigo:angular sanjo:jasmine angular:angular-mocks                     # works
meteor add sanjo:jasmine urigo:angular angular:angular-mocks                     # works

What. Well, ok, that makes some sense. It seems that for unrelated packages (I mean, with no dependencies between each other), Meteor seems to keep them in order of addition. angular-mocks required jasmine to be loaded earlier, and simply adding sanjo:jasmine seems to do the trick.

And since it does not matter if you do that in one line or three:

# will break
meteor add urigo:angular
meteor add angular:angular-mocks
meteor add sanjo:jasmine
# will work
meteor add sanjo:jasmine
meteor add urigo:angular
meteor add angular:angular-mocks

I have to admit that I actually hit that problem because I first created the app, added angular, added mocks, and only after running I noticed I forgot to add sanjo:jasmine.

Therefore, if you have problems with ReferenceError: module not defined when using angular:angular-mocks and sanjo:jasmine, you can simply try:

meteor remove angular:angular-mocks
meteor add angular:angular-mocks

Since you probably already have sanjo:jasmine, then it's enough to make sure angular-mocks have been added after that.

However, since there still is no relation between those two requested packages, Meteor can in fact load them in any order, and this trick doesn't guarantee anything, it can change with different Meteor versions. It just happens to work today.

Solution #4: create a dummy package that will pull mocks in order - ok!

If in the absence of dependencies Meteor doesn't reorder the list of loaded packages, then maybe:

Package.onUse(function(api) {
  api.versionsFrom('1.1.0.3');

  api.use('sanjo:jasmine', ['client'], {weak: true});
  api.use('urigo:angular', ['client'], {weak: true});
  api.use('angular:angular-mocks', ['client']);
});

Note I removed all JS files and just added a dep to angular-mocks. It turns out to be working regardless of the order of adding packages

meteor add urigo:angular my-mocks sanjo:jasmine                                  # works
meteor add my-mocks urigo:angular sanjo:jasmine                                  # works
meteor add urigo:angular sanjo:jasmine my-mocks                                  # works

As version numbers are not pinned on these dependencies, Meteor will select the most current ones from the directory, saving me from mantaining a clone of angular:angular-mocks.

Just as with solution #3, however, since there still is no relation between those two requested packages, Meteor can in fact load them in any order, and this trick doesn't guarantee anything, it can change with different Meteor versions. It just happens to work today.

Once important note to solution #4: since mocks are now implicitly provided by that dummy module, you have to remove angular:angular-mocks or else your app will fetch it on its own and nothing will change.

Afterthoughts

Of course it's disputable which solution is the best.. The best would be #1 but currently it is not possible. Oh, sorry, the very best would be to have angular-mocks don't expect having a jasmine before it or having angular-mocks-jasmine that depends on jasmine, but that's out of the scope for now.

When you hit this exact problem described here, then when picking between solution #3 and #4 you effectively have a choice between (#3):

meteor remove angular:angular-mocks
meteor add angular:angular-mocks

and (#4):

meteor remove angular:angular-mocks
meteor add my-dumy-mocks

and neither of them actually guarantee you much. So, pick #3, least surprise principle.

Similarly, when starting a new project, you have a choice of:

(#3) remember to add the dependencies in correct order
(#4) remember to add my-dummy-mocks instead of angular-mocks

and neither of them actually guarantee you much - but the first has much lower risk of suddenly starting to fail (preserving package-addition-order vs. preserving-order-of-dependencies). So, again, pick #3.

In case someone asked: this is why I am not publishing packages for #1 and #4. I also would currently advise against doing it, but of course there's no way of stopping anyone. If you consider doing it, then please, focus on lobbying for reverse weak dependencies (so sanjo:jasmine can fix that) or lobbying for specialized angular-mocks-jasmine (anyways, why just jasmine and mocha are so special?)

No comments: