Issues while writing tests for a component using i18n

Posted on 2019-08-04 in Aurelia

Some time ago, I wanted to add some tests (behavior and render) on an Aurelia component that uses the i18n plugin and more precisely the df attribute in the view to display a localized date.

Since it was a while back (I wanted to write it earlier but couldn't), my memory may not completely accurate. I hope it can help you nonetheless. If you have questions, please post a comment.

My component is pretty basic: it takes some bindable as inputs, define a time format to format the date correctly, has 3 methods to add custom behavior and that's it. You can view the model here and the view here.

Also note I am using TypeScript, Webpack and Jest. If you use different tooling, you may need to adapt the example a bit.

In my test, I setup my component like this:

component = StageComponent.withResources(PLATFORM.moduleName('../../src/resources/elements/aurss-article'))
    .inView(`
    <aurss-article
        value.bind="article"
        mark-article-as-read.bind="markArticleAsRead"
        mark-article-as-unread.bind="markArticleAsUnread"
        open-article.bind="openArticle"
    ></aurss-article>
    `).boundTo(viewModel);

await component.create(bootstrap);  // bootstrap comes from aurelia-bootstrapper

I got this error when running the tests: Error: No ValueConverter named "df" was found!. I then tried to register the component with:

component = StageComponent.withResources([
    PLATFORM.moduleName('../../src/resources/elements/aurss-article'),
    PLATFORM.moduleName('aurelia-i18n'),
])

But all I got was: Error: Error invoking RelativeTime. Check the inner error for details..

After reading tests in the plugin repo and this article (which solves a similar problem but the proposed solution didn't work for me), here is how I nailed it:

  • I created an test/helpers.ts file with the code below. Its purpose is to bootstrap the i18n plugin (it resembles what you do in main.ts):

    import {Aurelia, PLATFORM} from 'aurelia-framework';
    import {Backend} from 'aurelia-i18n';
    
    export const prepareI18nComponent = (component) => {
        component.bootstrap((aurelia: Aurelia) => {
            return aurelia.use.standardConfiguration()
                .plugin(PLATFORM.moduleName('aurelia-i18n'), (instance) => {
                    const aliases = ['t', 'i18n'];
    
                    // register backend plugin
                    instance.i18next.use(Backend.with(aurelia.loader));
                    const config = {
                        resources: {
                            en: {
                                translation: {
                                    hello: undefined,
                                },
                            },
                        },
                        skipTranslationOnMissingKey: true,
                    };
    
                    return instance.setup(Object.assign({
                        attributes: aliases,
                        backend: {
                            loadPath: './locales/{{lng}}/{{ns}}.json',
                        },
                        debug: false,
                        defaultNS: 'translation',
                        fallbackLng: 'en',
                        interpolation: {
                            prefix: '{{',
                            suffix: '}}',
                        },
                        lng: 'en',
                    }, config));
                });
        });
    };
    
  • In the beforeEach function, I now setup the component like this:

    beforeEach(async (done) => {
        viewModel = {
            article: createArticle(),
            markArticleAsRead: jest.fn(),
            markArticleAsUnread: jest.fn(),
            openArticle: jest.fn(),
        };
    
        component = StageComponent.withResources(PLATFORM.moduleName('../../src/resources/elements/aurss-article'))
            .inView(`
            <aurss-article
                value.bind="article"
                mark-article-as-read.bind="markArticleAsRead"
                mark-article-as-unread.bind="markArticleAsUnread"
                open-article.bind="openArticle"
            ></aurss-article>
            `).boundTo(viewModel);
    
        prepareI18nComponent(component);  // Bootstrap i18n
    
        await component.create(bootstrap);
    
        done();
    });
    
  • I also configured jest to load a file name test/jest-pretest.ts before it executes the tests suite by adding, under the jest section of my package.json file:

    "setupFiles": [
        "<rootDir>/test/jest-pretest.ts",
        "jest-localstorage-mock"
    ]
    

    This file contains:

    import {Options} from 'aurelia-loader-nodejs';
    import {globalize} from 'aurelia-pal-nodejs';
    import 'aurelia-polyfills';
    import * as IntlPolyfill from 'intl';
    import * as path from 'path';
    
    Options.relativeToDir = path.join(__dirname, 'unit');
    globalize();
    
    (global as any).navigator = {};
    
    global.Intl.NumberFormat   = IntlPolyfill.NumberFormat;
    global.Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
    (window as any).Intl = global.Intl;
    
    (window as any).Intl.NumberFormat   = IntlPolyfill.NumberFormat;
    (window as any).Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
    

    It is meant to be sure the Intl API used by the plugin is correctly defined during the tests.

That's it! You can also take a look at the full project code in gitlab if you need more details: https://gitlab.com/Jenselme/aurss