cloudchen / grunt-template-jasmine-requirejs

RequireJS template for grunt-contrib-jasmine task
111 stars 97 forks source link

How to get access to loaded AMD modules within each test file? #23

Closed Integralist closed 11 years ago

Integralist commented 11 years ago

Hi,

Here is my latest Gruntfile...

jasmine: {
            src: ['<%= dir.static_js %>**/*.js', 
                  '<%= dir.static_js %>specs/helpers/**/*.js', 
                  '!<%= dir.static_js %>specs/**/*Spec.js', 
                  '!<%= dir.static_js %>tests/**/*.js', 
                  '!<%= dir.static_js %>build.js', 
                  '!<%= dir.static_js %>compiled/all.js', 
                  '!<%= dir.static_js %>vendor/**/*.js', 
                  '!<%= dir.static_js %>**/bootstrap_desktop.js', 
                  '!<%= dir.static_js %>translation.js'],
            options: {
                specs: '<%= dir.static_js %>specs/**/*Spec.js',
                helpers: ['<%= dir.static_js %>specs/helpers/*Helper.js', 
                          '<%= dir.static_js %>specs/helpers/sinon.js'],
                host: 'http://127.0.0.1:8000/',
                template: require('grunt-template-jasmine-requirejs'),
                templateOptions: {
                    requireConfig: {
                        baseUrl: '<%= dir.static_js %>',
                        paths: {
                            'jquery-1': 'vendor/jquery-1/jquery.min',
                            'jquery': 'vendor/jquery-2/jquery.min',
                            'domReady': 'vendor/require/domReady',
                            'translation': 'module/translations/news'
                        },
                        name: 'define'
                    }
                }
            }
        }

This generates the following SpecRunner file...

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Jasmine Spec Runner</title>

  <link rel="stylesheet" type="text/css" href=".grunt/grunt-contrib-jasmine/jasmine.css">

  <script src="./.grunt/grunt-contrib-jasmine/jasmine.js"></script>

  <script src="./.grunt/grunt-contrib-jasmine/jasmine-html.js"></script>

  <script src="./tabloid/webapp/static/js/specs/helpers/SpecHelper.js"></script>

  <script src="./tabloid/webapp/static/js/specs/helpers/sinon.js"></script>

  <script>

      var require = {
  "baseUrl": "./tabloid/webapp/static/js/",
  "paths": {
    "jquery-1": "vendor/jquery-1/jquery.min",
    "jquery": "vendor/jquery-2/jquery.min",
    "domReady": "vendor/require/domReady",
    "translation": "module/translations/news"
  },
  "name": "define"
};

  </script>

  <script>
  function launchTest() {
    require([

        'bootstrap-ui' ,

        'config' ,

        'define' ,

        'deviceInspector' ,

        'locator/autoCompleteView' ,

        'locator/autoCompleteView.js ' ,

        'locator/bootstrap' ,

        'locator/locatorView' ,

        'locator/main' ,

        'locator/stats' ,

        'module/ajaxForm/ajaxForm' ,

        'module/ajaxForm/ajaxFormView' ,

        'module/analytics/analytics_client' ,

        'module/base' ,

        'module/bootstrap-desktop' ,

        'module/bootstrap' ,

        'module/components/featuresAndAnalysis' ,

        'module/components/mostPopular' ,

        'module/components/topStoriesPromo' ,

        'module/deviceInspector' ,

        'module/featureDetector' ,

        'module/fontface' ,

        'module/history/hashed' ,

        'module/history/history' ,

        'module/history/native' ,

        'module/hyperpuff/hyperpuff' ,

        'module/hyperpuff/minihyper' ,

        'module/imageenhancer' ,

        'module/index' ,

        'module/live-event-component' ,

        'module/live-event' ,

        'module/liveEvent/timeline' ,

        'module/liveEvent/timelineView' ,

        'module/local/local' ,

        'module/local/localView' ,

        'module/localnewsandweather' ,

        'module/media/empFacade' ,

        'module/media/mediaHandler' ,

        'module/media/mediaLoader' ,

        'module/media/mediaRegistry' ,

        'module/media/piratePlayer' ,

        'module/media/piratePlayerView' ,

        'module/mediaAssetPage/api' ,

        'module/mediaAssetPage/assetView' ,

        'module/mediaAssetPage/listView' ,

        'module/mediaAssetPage/map' ,

        'module/mediaAssetPage/mediaAssetPage' ,

        'module/mediaAssetPage/pinningView' ,

        'module/nav/nav' ,

        'module/nav/navManager' ,

        'module/poller' ,

        'module/readabilityCheck' ,

        'module/rollingNews' ,

        'module/stats/siteCatalyst' ,

        'module/stats/statsConfig' ,

        'module/stats/statsSubscriber' ,

        'module/story' ,

        'module/stylesheetloader' ,

        'module/tabbed/panel' ,

        'module/tabbed/tab' ,

        'module/tabbed/tabbed' ,

        'module/timestamp' ,

        'module/transclude' ,

        'module/translations/afrique' ,

        'module/translations/arabic' ,

        'module/translations/hausa' ,

        'module/translations/hindi' ,

        'module/translations/indonesia' ,

        'module/translations/mundo' ,

        'module/translations/news' ,

        'module/translations/russian' ,

        'module/translator' ,

        'module/travel/travel' ,

        'module/travel/travelView' ,

        'module/urlShrinker' ,

        'module/weather/weather' ,

        'module/weather/weatherLocation' ,

        'module/weather/weatherLocationView' ,

        'module/weather/weatherView' ,

        'specs/helpers/SpecHelper' ,

        'specs/helpers/sinon' ,

        'translator' 

    ],
    function(){
      require(['./tabloid/webapp/static/js/specs/AjaxFormSpec.js','./tabloid/webapp/static/js/specs/AjaxFormViewSpec.js','./tabloid/webapp/static/js/specs/BasicSpec.js','./tabloid/webapp/static/js/specs/BootstrapSpec.js','./tabloid/webapp/static/js/specs/DOMSpec.js','./tabloid/webapp/static/js/specs/DeviceInspectorSpec.js','./tabloid/webapp/static/js/specs/EmpFacadeSpec.js','./tabloid/webapp/static/js/specs/FontFaceSpec.js','./tabloid/webapp/static/js/specs/HashedHistorySpec.js','./tabloid/webapp/static/js/specs/HistorySpec.js','./tabloid/webapp/static/js/specs/HyperPuffSpec.js','./tabloid/webapp/static/js/specs/ImageEnhancerSpec.js','./tabloid/webapp/static/js/specs/LiveEventComponentSpec.js','./tabloid/webapp/static/js/specs/LocalSpec.js','./tabloid/webapp/static/js/specs/LocalViewSpec.js','./tabloid/webapp/static/js/specs/MapSpec.js','./tabloid/webapp/static/js/specs/MediaAssetPageApiSpec.js','./tabloid/webapp/static/js/specs/MediaHandlerSpec.js','./tabloid/webapp/static/js/specs/MediaLoaderSpec.js','./tabloid/webapp/static/js/specs/MiniHyperSpec.js','./tabloid/webapp/static/js/specs/NativeHistorySpec.js','./tabloid/webapp/static/js/specs/NavManagerSpec.js','./tabloid/webapp/static/js/specs/NavSpec.js','./tabloid/webapp/static/js/specs/PollerAsyncSpec.js','./tabloid/webapp/static/js/specs/PollerSpec.js','./tabloid/webapp/static/js/specs/RollingNewsSpec.js','./tabloid/webapp/static/js/specs/SinonSpec.js','./tabloid/webapp/static/js/specs/StatsSubscriberSpec.js','./tabloid/webapp/static/js/specs/TabbedSpec.js','./tabloid/webapp/static/js/specs/TimelineSpec.js','./tabloid/webapp/static/js/specs/TimelineViewSpec.js','./tabloid/webapp/static/js/specs/TimestampSpec.js','./tabloid/webapp/static/js/specs/TopStoriesPromoSpec.js','./tabloid/webapp/static/js/specs/TranscludeSpec.js','./tabloid/webapp/static/js/specs/TranslatorSpec.js','./tabloid/webapp/static/js/specs/TravelSpec.js','./tabloid/webapp/static/js/specs/TravelViewSpec.js','./tabloid/webapp/static/js/specs/WeatherLocationSpec.js','./tabloid/webapp/static/js/specs/WeatherLocationViewSpec.js','./tabloid/webapp/static/js/specs/WeatherSpec.js','./tabloid/webapp/static/js/specs/WeatherViewSpec.js','./.grunt/grunt-contrib-jasmine/reporter.js'], function(){
        require(['./.grunt/grunt-contrib-jasmine/jasmine-helper.js'], function(){
          // good to go! Our tests should already be running.
        })
      })
    }
    )
  }
  </script>

  <script src=".grunt/grunt-contrib-jasmine/require.js"></script>
  <script>
      require.onError = function(error) {
      var message = error.requireType + ': ';
      if (error.requireType === 'scripterror' || error.requireType === 'notloaded' && error.requireModules) {
        message += 'Illegal path or script error: ' + '[\'' + error.requireModules.join("', '") + '\']';
      } else {
        message += error.message;
      }

      throw Error(message);
    };
  </script>

  <script>
  launchTest();
  </script>

</head>
<body>
</body>
</html>

The issue I have is that the AMD modules that are loaded before the tests are run aren't available within the tests.

I'm not sure how I access the loaded AMD modules within each test file?

For example, here is one test file...

describe('abc', function(){
    beforeEach(function(){
        $('body').append('<div id="js-injected"></div>');

        // this didn't look to work for me -> is there any other options?
        define(['module/bootstrap'], $.proxy(function(boot) {
            this.boot = boot;
        }, this));
    });

    afterEach(function(){
        $('#js-injected').remove();
    });

    it('should allow me to access the bootstrap AMD module', function(){
        console.log(this.boot);
    });
});
Integralist commented 11 years ago

Ah, OK it looks like this works...

describe('x', function(){
    beforeEach(function(){
        $('body').append('<div id="js-injected"></div>');
        this.news = require('module/bootstrap');
    });

    afterEach(function(){
        $('#js-injected').remove();
    });

    it('should x', function(){
        console.log('this.news.$', this.news.$);
    });
});

I'm going to keep testing to see if there is any other issue that crops up!

Integralist commented 11 years ago

One question though, is this a good idea? Is there not a better way?

Integralist commented 11 years ago

The current issue I'm having is getting jQuery (which is loaded by the time the tests run) to successfully run its ajax method.

For some reason it doesn't run the success callback or the error callback, and I'm not sure why that would be?

If I run console.log(this.news.$.ajax) I can see in the Terminal response the content of the ajax method. But the more bare bones usage doesn't work...

describe('AJAX Form', function(){
    beforeEach(function(){
        $('body').append('<div id="js-injected"></div>');
        this.news = require('module/bootstrap');
    });

    afterEach(function(){
        $('#js-injected').remove();
    });

    it('should run the error handler', function(){
        this.news.$.ajax({
            url: 'file-that-doesnt-exist.js',
            success: function(response) {
                console.log(response);
            },
            error: function() {
                console.log('error occurred');
            }
        });
    });
});

It's a big issue that I noticed first when I tried to use Sinon to mock the AJAX functionality (which doesn't work either). I removed the Sinon code and tested the bare bones ajax functionality to find it still didn't work.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Jasmine Spec Runner</title>

  <link rel="stylesheet" type="text/css" href=".grunt/grunt-contrib-jasmine/jasmine.css">

  <script src="./.grunt/grunt-contrib-jasmine/jasmine.js"></script>

  <script src="./.grunt/grunt-contrib-jasmine/jasmine-html.js"></script>

  <script src="./tabloid/webapp/static/js/specs/helpers/SpecHelper.js"></script>

  <script src="./tabloid/webapp/static/js/specs/helpers/sinon.js"></script>

  <script>

      var require = {
  "baseUrl": "./tabloid/webapp/static/js/",
  "paths": {
    "jquery-1": "vendor/jquery-1/jquery.min",
    "jquery": "vendor/jquery-2/jquery.min",
    "domReady": "vendor/require/domReady",
    "translation": "module/translations/news"
  },
  "name": "define"
};

  </script>

  <script>
  function launchTest() {
    require([

        'bootstrap-ui' ,

        'config' ,

        'define' ,

        'deviceInspector' ,

        'locator/autoCompleteView' ,

        'locator/autoCompleteView.js ' ,

        'locator/bootstrap' ,

        'locator/locatorView' ,

        'locator/main' ,

        'locator/stats' ,

        'module/ajaxForm/ajaxForm' ,

        'module/ajaxForm/ajaxFormView' ,

        'module/analytics/analytics_client' ,

        'module/base' ,

        'module/bootstrap-desktop' ,

        'module/bootstrap' ,

        'module/components/featuresAndAnalysis' ,

        'module/components/mostPopular' ,

        'module/components/topStoriesPromo' ,

        'module/deviceInspector' ,

        'module/featureDetector' ,

        'module/fontface' ,

        'module/history/hashed' ,

        'module/history/history' ,

        'module/history/native' ,

        'module/hyperpuff/hyperpuff' ,

        'module/hyperpuff/minihyper' ,

        'module/imageenhancer' ,

        'module/index' ,

        'module/live-event-component' ,

        'module/live-event' ,

        'module/liveEvent/timeline' ,

        'module/liveEvent/timelineView' ,

        'module/local/local' ,

        'module/local/localView' ,

        'module/localnewsandweather' ,

        'module/media/empFacade' ,

        'module/media/mediaHandler' ,

        'module/media/mediaLoader' ,

        'module/media/mediaRegistry' ,

        'module/media/piratePlayer' ,

        'module/media/piratePlayerView' ,

        'module/mediaAssetPage/api' ,

        'module/mediaAssetPage/assetView' ,

        'module/mediaAssetPage/listView' ,

        'module/mediaAssetPage/map' ,

        'module/mediaAssetPage/mediaAssetPage' ,

        'module/mediaAssetPage/pinningView' ,

        'module/nav/nav' ,

        'module/nav/navManager' ,

        'module/poller' ,

        'module/readabilityCheck' ,

        'module/rollingNews' ,

        'module/stats/siteCatalyst' ,

        'module/stats/statsConfig' ,

        'module/stats/statsSubscriber' ,

        'module/story' ,

        'module/stylesheetloader' ,

        'module/tabbed/panel' ,

        'module/tabbed/tab' ,

        'module/tabbed/tabbed' ,

        'module/timestamp' ,

        'module/transclude' ,

        'module/translations/afrique' ,

        'module/translations/arabic' ,

        'module/translations/hausa' ,

        'module/translations/hindi' ,

        'module/translations/indonesia' ,

        'module/translations/mundo' ,

        'module/translations/news' ,

        'module/translations/russian' ,

        'module/translator' ,

        'module/travel/travel' ,

        'module/travel/travelView' ,

        'module/urlShrinker' ,

        'module/weather/weather' ,

        'module/weather/weatherLocation' ,

        'module/weather/weatherLocationView' ,

        'module/weather/weatherView' ,

        'specs/helpers/SpecHelper' ,

        'specs/helpers/sinon' ,

        'translator' 

    ],
    function(){
      require(['./tabloid/webapp/static/js/specs/AjaxFormSpec.js','./.grunt/grunt-contrib-jasmine/reporter.js'], function(){
        require(['./.grunt/grunt-contrib-jasmine/jasmine-helper.js'], function(){
          // good to go! Our tests should already be running.
        })
      })
    }
    )
  }
  </script>

  <script src=".grunt/grunt-contrib-jasmine/require.js"></script>
  <script>
      require.onError = function(error) {
      var message = error.requireType + ': ';
      if (error.requireType === 'scripterror' || error.requireType === 'notloaded' && error.requireModules) {
        message += 'Illegal path or script error: ' + '[\'' + error.requireModules.join("', '") + '\']';
      } else {
        message += error.message;
      }

      throw Error(message);
    };
  </script>

  <script>
  launchTest();
  </script>

</head>
<body>
</body>
</html>
cloudchen commented 11 years ago

console.log() invocations in your spec or source code won't be shown in grunt jasmine task Use console.info() instead, it makes sense to ignore those debugging information in order to filtering annoying output.

Integralist commented 11 years ago

Ultimately I think the primary issue here is that the modules are loaded fine but there is no 'good' way to access the return value from the module. Do you or @jsoverson have any ideas to achieve that? Because it seems most of my issues stem from that primary issue :-(

cloudchen commented 11 years ago

Sorry, I don't understand your ultimate needs. In general, module definition should be treated as static object, so it should return same thing when access it every time, just like import directive does in other languages, such as python. We use require('module/A') or ['module/A'] by define() to explicitly get it in AMD style. What's meaning of

there is no 'good' way to access the return value from the module

Integralist commented 11 years ago

@cloudchen I'm going to look into this more to see if I can find the issue with jQuery not running properly within the context of the unit test.

But just to clarify, as far as you're concerned, doing the following is fine and should be the way to go (and by that I'm specifically referring to the use of this.news = require('module/bootstrap'); to access the module that has been loaded by the spec runner)...

describe('AJAX Form', function(){
    beforeEach(function(){
        $('body').append('<div id="js-injected"></div>');
        this.news = require('module/bootstrap');
    });

    afterEach(function(){
        $('#js-injected').remove();
    });

    it('should run the error handler', function(){
        this.news.$.ajax({
            url: 'file-that-doesnt-exist.js',
            success: function(response) {
                console.log(response);
            },
            error: function() {
                console.log('error occurred');
            }
        });
    });
});
jsoverson commented 11 years ago

@Integralist var foo = require('foo') is the recommended way.

Also, a disclaimer, this template is just a way not the way to test requirejs/amd modules. It follows some accepted practices but anyone is able to make their own template to accomodate different testing styles. The template is minimal and can easily be changed to accomodate different styles.

Integralist commented 11 years ago

@jsoverson thanks for the feedback. I'm trying to figure out why jQuery isn't working within the context of the test when loaded like so...

describe('AJAX Form', function(){
    beforeEach(function(){
        $('body').append('<div id="js-injected"></div>');
        this.news = require('module/bootstrap');
    });

    afterEach(function(){
        $('#js-injected').remove();
    });

    it('should run the error handler', function(){
        console.info(this.news.$.ajax);
        this.news.$.ajax({
            url: 'file-that-doesnt-exist.js',
            success: function(response) {
                console.info(response);
            },
            error: function() {
                console.info('error occurred');
            }
        });
    });
});

The 'module/bootstrap' looks like this...

define([
    'jquery',
    'config'
], function(
    jquery,
    config
) {
    var news = {
        $: jquery,
        config: config
    };

    return news;
});

So there is nothing complicated going on there.

When I run a console.info on this.news.$.ajax I can see the content of the AJAX method but yet running a basic test where the method tries to get a file that doesn't exist, the error callback isn't invoked (no content is displayed in the terminal to suggest that it has been run).

Any ideas?

cloudchen commented 11 years ago

Finally, I see what you concerned. Because you are testing async method rather than sync ones. You should use wait() or waitFor() methods of jasmine to test async method. Or use sinon.js to mock ajax result and manually respond it before expect() invocation. Take a look at associated documentations of jasmine.

On Wednesday, May 1, 2013, Mark McDonnell wrote:

@jsoverson https://github.com/jsoverson thanks for the feedback. I'm trying to figure out why jQuery isn't working within the context of the test when loaded like so...

describe('AJAX Form', function(){ beforeEach(function(){ $('body').append('

'); this.news = require('module/bootstrap'); });

afterEach(function(){
    $('#js-injected').remove();
});

it('should run the error handler', function(){
    console.info(this.news.$.ajax);
    this.news.$.ajax({
        url: 'file-that-doesnt-exist.js',
        success: function(response) {
            console.info(response);
        },
        error: function() {
            console.info('error occurred');
        }
    });
});});

The 'module/bootstrap' looks like this...

define([ 'jquery', 'config'], function( jquery, config) { var news = { $: jquery, config: config };

return news;});

So there is nothing complicated going on there.

When I run a console.info on this.news.$.ajax I can see the content of the AJAX method but yet running a basic test where the method tries to get a file that doesn't exist, the error callback isn't invoked (no content is displayed in the terminal to suggest that it has been run).

Any ideas?

— Reply to this email directly or view it on GitHubhttps://github.com/jsoverson/grunt-template-jasmine-requirejs/issues/23#issuecomment-17272373 .

Integralist commented 11 years ago

@cloudchen I was mocking the ajax with sinon but the results weren't working as expected (seemed like nothing was being mocked) so I reverted to using real ajax call. I'll double check my code.

Integralist commented 11 years ago

@cloudchen @jsoverson I updated my example code this morning and found that it fails still even though I'm sure it shouldn't...

describe('AJAX Form', function(){
    beforeEach(function(){
        $('body').append('<div id="js-injected"></div>');
        this.news = require('module/bootstrap');

        // Sinon set-up
        this.server = sinon.fakeServer.create();
    });

    afterEach(function(){
        $('#js-injected').remove();
    });

    it('should run the error handler', function(){
        var spy_success = sinon.spy(),
            spy_error = sinon.spy();

        this.server.respondWith([200, {}, JSON.stringify({reply: 'success', body: 'test'})]);
        // this.server.respondWith([404, {}, JSON.stringify({reply: 'lulz', body: 'test'})]);

        this.news.$.ajax({
            url: 'file-that-doesnt-exist.js',
            success: spy_success,
            error: spy_error
        });

        this.server.respond();

        expect(spy_success.called).toBeTruthy();
    });
});
cloudchen commented 11 years ago

I don't know what happened with your environment. I copied above code and passed in my env. Check you environment first.