0

Expectation: When testing a template or the outputted directive / component the translations such as <h1>{{home.title | translate}}</h1> should be translated to show the actual text <h1>Home Page</h1>.

Now after a lot of digging I have been able to make it work by manually putting the translations I need in my test.

Example: Current test uses manual translation setup in test, The key here is $translateProvider.translations.

(function() {
'use strict';

describe('home component', function() {
  var rootscope, compile, directiveElement;
  beforeEach(module('Templates'));
  beforeEach(module('myApp'));

  beforeEach(module('tmh.dynamicLocale'), function () {
    tmhDynamicLocaleProvider.localeLocationPattern('base/angular/i18n/angular-locale_{{locale}}.js');
  });

  beforeEach(module('pascalprecht.translate', function ($translateProvider) { 
    $translateProvider.translations('en', {
      "home":{
         "title": "Home page"
      }
    });
  }));

  beforeEach(inject(function(_$rootScope_, _$compile_) {
    rootscope = _$rootScope_.$new();
    compile = _$compile_;
  }));

  function getCompiledElement(){
    var element = angular.element('<home-component></home-component');
    var compiledElement = compile(element)(rootscope);
    rootscope.$digest();
    return compiledElement;
  }

  describe('home', function () {
    it('should have template defined', function () {
     directiveElement = getCompiledElement();
     console.log('my component compiled', directiveElement);
    });
  });
 });
 })();

Output generated is correct:

Home page

Now this above compiles my component and shows the text as translated correctly instead of seeing the curly braces and the keys. Now in a realistic application it is not great to have to manually take the translations you need and put them in, as well as translations may change and you may forget to update your test.

I would like my tests to use the actual static json translation files

resources
  | locale-en_US.json

I have tried using the below code however with it being asyncronous it does not load by the times the tests are going. I need a way to either wait until the files are loaded or a different way to load the files into the $translateProvider.

$translateProvider.useStaticFilesLoader({
  prefix: 'app/resources/locale-', // path to translations files
  suffix: '.json'
});   

I also tried loading the language json files through karma.conf.js as shown below.

Files[
   ...
   { pattern: 'app/resources/angular-i18n/*.js', included: true, served: true },
   {pattern: 'app/resources/*.json', included: true, served: true},
 ]

I know there has to be a way this can work however I have not found a solution yet. Some say their solution works however I have tried using different plugins and still seems to not work.

UPDATE: Custom-Loader

I am reading into creating a custom loader for the $translateProvider. I am not sure how to build it to handle the way I want so it can be used for testing properly but in case others are looking into this then this may be a spot to look at.

$provide.factory('customLoader', function ($q) {
    return function () {
      var deferred = $q.defer();
      deferred.resolve({});
      return deferred.promise;
    };
  });

  $translateProvider.useLoader('customLoader');
halfer
  • 19,824
  • 17
  • 99
  • 186
L1ghtk3ira
  • 3,021
  • 6
  • 31
  • 70
  • The reason why you didn't find is likely because this isn't conventional scenario. This is usually done in e2e tests against real DOM and real data. It doesn't make sense to test component unit like that. – Estus Flask Feb 13 '18 at 20:57
  • Our project under goes many testing phases. One is test coverage has to always increase or our builds will fail as part of our managers decision. The point of these tests is to ensure with the data passed that the components, directives are being built and behaving correctly. The want tests of the HTML on our end and this is the only way for a decent test in its compiled form. Our translations files are all static, they want the logged out components so they can verify under scenarios that the translations are correct as part of content-writer review. Not my decision. – L1ghtk3ira Feb 13 '18 at 21:01
  • Wether this is also conventional is irelevant as it changes from project to project and requirements. – L1ghtk3ira Feb 13 '18 at 21:03
  • The thing you're describing is what Protractor e2e tests are for. Doing that in Karma and unit tests will make them hackier, slower and less efficient. A cleaner approach would be to mock `translate` pipe and make output values from JSON files, similarly to how third-party translator does it, while real pascalprecht.translate is never included. – Estus Flask Feb 13 '18 at 21:15
  • Protractor is good yes however they want the usit tests to test individual components as well as their functionality. Can you provide an example of your idea? – L1ghtk3ira Feb 13 '18 at 21:19
  • Hope this helps. As for Protractor, you're not limited to real app for testing. It's not uncommon to set up additional HTML pages and app modules for e2e/integration tests, so you can test whatever you need there in production environment (even if you tested translations with Karma, there's no guarantee that JSON files will load in real life). – Estus Flask Feb 13 '18 at 22:02
  • As for how to make real requests with Karma and ngMock, see https://stackoverflow.com/questions/42331583/making-real-requests-to-http-server-in-angularjs-unit-integration-tests – Estus Flask Feb 13 '18 at 22:08

2 Answers2

0

In order for unit tests to be most efficient for trouble solving, a unit should be isolated from other units. This way if a test becomes red, failed unit can be unambiguously determined. This is the reason why it's preferable to explicitly mock everything in unit tests and provide fixtures instead of relying on real data.

The use of third-party units breaks isolation, because their problems (bugs, package versions) cannot be distinguished from problems in units themselves. This makes debugging more costly.

The dependency on pascalprecht.translate can be eliminated in unit tests by providing mocked filter that will perform tested functionality in simplified, controllable way. To be tested against real data, Karma should be configured to support modules, e.g. karma-commonjs preprocessor, this way JSON data can be loaded directly and not through XHR request.

Lodash get is a good candidate to parse dot-separated paths, similarly to how translation service does this:

  var translationsEn = require('.../locale-en_US.json');
  ...
  beforeEach(angular.mock.module({ translateFilter: function (path) {
    return _.get(translationsEn, path);
  }));

This will mock translate filter (which is translateFilter service internally) with simple workaround. This will work as long as values doesn't use extended translation features like pluralization.

Alternatively, real translation service can be fixed similarly to how the guide suggests, but making use of CommonJS to load JSON files:

beforeEach(angular.mock.module(function ($translateProvider) {
  $translateProvider.translations('en', translationsEn);
}));

angular.mock.module should be used instead of module to avoid name collisions when CommonJS modules are in use.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
0

Ok after digging deeper I think I came up with a solution that is not too hacky and fairly clean. First I installed and followed the instructions for this plugin Karma-fixture https://www.npmjs.com/package/karma-fixture

After setting up the fixture I call the global variable created by karma-fixture and use it in my test to grab the json file I want as well as load it cleanly into the $translateProvider.translations as shown below.

Note: Templates and fixture as well as files are important:

beforeEach(module('pascalprecht.translate', function ($translateProvider) {

   $translateProvider.translations('en', 
     fixture.load('app/resources/locale-en_US.json')
   );

   $translateProvider.preferredLanguage('en');
}));

Full working example of A Jasmine Unit test that compiles a component with translations below.

Karma.conf.js: File

module.exports = function(config) {
  'use strict';

   config.set({
     autoWatch: true,
     basePath: '../',

     frameworks: ['jasmine', 'fixture'],

     files: [
       // bower:js
       ... <-Bower files Here ->
       // endbower
       {pattern: 'app/resources/angular-i18n/*.js', included: true, served: true },
       {pattern: 'app/resources/*.json', included: true, served: true},
       'app/**/*.app.js',     // First load main module
       'app/**/*.module.js',  // Then Load all modules
       'app/**/*.tpl.html',   // Then Load all html pages
       'app/**/!(*.spec).js', // Then load all javascript files that are not tests
       'app/**/*.spec.js'     // Finally load all tests
     ],

     exclude: [
       'app/css/**/*.js',
       'app/js/**/*.js',
       'app/styles/vendor/**/*.js'
     ],

     port: 8080,

     browsers: ['PhantomJS'],

     plugins: [
       'karma-phantomjs-launcher',
       'karma-chrome-launcher', 
       'karma-jasmine',
       'karma-coverage',
       'karma-ng-html2js-preprocessor',
       'karma-json-fixtures-preprocessor',
       'karma-fixture'
     ],

     preprocessors: {
       'app/**/*.js': 'coverage',
       'app/**/*.html': 'ng-html2js',
       'app/resources/*.json'   : ['json_fixtures']
     },

     ngHtml2JsPreprocessor: {
       'moduleName': 'Templates',
       'stripPrefix': 'app/'
     },

     jsonFixturesPreprocessor: {variableName: '__json__'},

     reporters: ['progress', 'coverage'],
     singleRun: false,
     colors: true,
     logLevel: config.LOG_INFO,
   });
 };

Jasmine Unit Test: Bare Bones with component and translations utilized

(function() {
  'use strict';

  describe('Home component', function() {
    var rootscope, compile, componentElement;

    beforeEach(module('Templates'));
    beforeEach(module('app'));

    beforeEach(module('pascalprecht.translate', function ($translateProvider) {
      $translateProvider.translations('en', 
        fixture.load('app/resources/locale-en_US.json')
      );

      $translateProvider.preferredLanguage('en');
    }));

    beforeEach(inject(function(_$rootScope_, _$compile_) {
      rootscope = _$rootScope_.$new();
      compile = _$compile_;
    }));

    function getCompiledElement(){
      var element = angular.element('<home-component></home-component>');
      var compiledElement = compile(element)(rootscope);
      rootscope.$digest();
      return compiledElement;
    }

    describe('Home tests', function () {

      it('should have component defined', function () {
        componentElement = getCompiledElement();
        console.log('Compiled component with translations', componentElement);
        expect(componentElement).toBeDefined()
      });
    });
  });
})();

The above implementation will compile your component (can pass bindings etc) as well as utilize the translator and show your translations in your component in the console.

Instead of seeing: <h1>{{home.title | translate}}</h1>, you will now see: <h1>Home Page</h1>

halfer
  • 19,824
  • 17
  • 99
  • 186
L1ghtk3ira
  • 3,021
  • 6
  • 31
  • 70