javascriptmeteorjasminemeteor-velocitymeteor-jasmine

Requiring timeouts when testing Meteor with Velocity and Jasmine


Pretty new to meteor, velocity and jasmine so not sure if I am doing something wrong, using Jasmine for something it's not designed for, or this is just the way it works.

I am finding I need to set timeouts for pretty much all of my tests in order to get them to pass. Should this be the case or am I doing something incorrectly?

For example some tests I am running to check validation messages:

    describe("add quote validation", function() {
      beforeEach(function (done) {
        Router.go('addQuote');
        Tracker.afterFlush(function(){
          done();
        });
      });

      beforeEach(waitForRouter);

      it("should show validation when Quote is missing", function(done) {
        $('#quote').val('');
        $('#author').val('Some author');
        Meteor.setTimeout(function(){
          $('#addQuoteBtn').click();
        }, 500);
        Meteor.setTimeout(function(){
          expect($('.parsley-custom-error-message').text()).toEqual("Quote can't be empty.");
          done();
          }, 500);
      });
    }

Solution

  • OK, we've had this exact same problem and devised a pretty elegant solution to it, that doesn't require timeouts and is the fastest way to run your tests. Basically, we use one of two strategies, depending on what screen elements you're waiting for.

    All code goes into tests/mocha/client/lib.coffee, not 100% what the Jasmine equivalent is, but it should be available to all client test code. I've left it in Coffeescript, but you can compile it on coffeescript.org into Javascript, it should work fine as well.

    If whatever you do (routing or something else like changing a reactive variable) causes a Template to (re)render, you can use the Template.<your_template>.rendered hook to detect when it is finished rendering. So, we've added the following function in lib.coffee:

    @afterRendered = (template,f)->
        cb = template.rendered
        template.rendered = ->
          cb?()
          template.rendered = cb
          f?()
          return
        return
    

    What does it do? It basically "remembers" the original rendered callback and temporarily replaces it with one that calls an extra function after the template is rendered and the original callback is called. It needs to do this sort of housekeeping to avoid breaking any code that may have depended on the rendered callback, as you're basically messing with the Meteor code directly.

    In your test, you can then do something like this:

     it.only "should check stuff after routing", (done)->
        try
          Router.go "<somewhere>"
          afterRendered Template.<expected_template>, ->
            <your tests here>
            done()
        catch e
          done(e)
    

    I'd recommend the try-catch as well, as I've noticed asynchronous errors don't always make it into the velocity system, merely giving you a timeout failure.

    OK, then there are things that don't actually re-render, but are generated with JS or by some kind of "show/hide" mechanism. For that, you do need some kind of timeout, but you can reduce the "time cost" of the timeout by using a polling mechanism.

    # evaluates if a JQuery element is visible or not
    $.fn.visible = -> this.length > 0 and this.css('display') isnt 'none'
    
    # This superduper JQuery helper function will trigger a function when an element becomes visible (display != none). If the element is already visible, it triggers immediately. 
    $.fn.onVisible = (fn,it)->
        sel = this.selector
        if this.visible()
          console.log "Found immediately"
          fn?(this)
        else
          counter = 0
          timer = setInterval ->
            counter++
            el = $(sel)
            if el.visible()
              fn?(el)
              clearInterval timer
              console.log "Found on iteration #{counter}"
            else
              it?(el)
          , 50
    

    You can remove the console logging and secondary it iterator function if you like, they're not important. This allows you to do something like this in your test:

    $('#modalId').onVisible (el)->
      <tests here>
      done()
    , (el)->
      console.log "Waiting for #{el.selector}"
    

    You can remove the second function if you want, it is the it iterator function mentioned above. However, do note that this particular code works with "display: hidden" as the marker for invisibility (Bootstrap does this). Change it if your code uses another mechanism to hide/show parts.

    Works like a charm for us!