aurelia / testing

Simplifies the testing of UI components by providing an elegant, fluent interface for arranging test setups along with a number of runtime debug/test helpers.
MIT License
40 stars 27 forks source link

<compose> inside a view doesn't render during Jest tests run (seems to become ready *after* execution) #86

Closed silbinarywolf closed 5 years ago

silbinarywolf commented 5 years ago

I'm submitting a bug report

Library Version:

Please tell us about your environment:

Current behavior:

Expected/desired behavior:

Example code (stripped down)

In the browser, my attached() method works fine. However in Jest, it does not and my error code is thrown.

datepicker.ts

@inject(DOM.Element, Form, BindingEngine, DateFormatValueConverter)
@customElement('a-datepicker')
@useView(PLATFORM.moduleName('./field.html'))
export class Datepicker extends Field {
    public readonly fieldView: string = PLATFORM.moduleName('./datepicker.html');

    public attached() {
        super.attached();
        console.warn(this.element.innerHTML);
        console.warn('fieldView', this.fieldView);
        this.inputElem = this.element.querySelector('input');
        if (!this.inputElem) {
            throw new Error('initDatepicker: Cannot find <input> element in <a-datepicker>.');
        }
    }
}

field.html

<template>
    <!-- Form control -->
    <compose
        view.bind="fieldView"
    ></compose>
</template>

datepicker.html (ie. fieldView)

<template>
    <input
        type="text"
    />
</template>

Console output when running Jest

As you can see from the stack trace below, my throw new Error code executes and then NodeJsLoader.prototype.loadModule() is called.

  console.error null:null
    Error: initDatepicker: Cannot find <input> element in <a-datepicker>.
        at Datepicker.Object.<anonymous>.Datepicker.attached (redacted\src\components\field\common\datepicker.ts:xx:10)
        at Controller.attached (redacted\node_modules\aurelia-templating\dist\commonjs\aurelia-templating.js:3761:22)
        at View.attached (redacted\node_modules\aurelia-templating\dist\commonjs\aurelia-templating.js:1789:22)
        at TemplatingEngine.enhance (redacted\node_modules\aurelia-templating\dist\commonjs\aurelia-templating.js:5195:10)
        at redacted\node_modules\aurelia-framework\dist\commonjs\aurelia-framework.js:176:28
        at new Promise (<anonymous>)
        at Aurelia.enhance (redacted\node_modules\aurelia-framework\dist\commonjs\aurelia-framework.js:174:12)
        at redacted\node_modules\aurelia-testing\dist\commonjs\component-tester.js:52:36
        at <anonymous>

  console.warn node_modules/aurelia-loader-nodejs/dist/commonjs/aurelia-loader-nodejs.js:286
    module:  components/field/common/datepicker.html

  console.warn node_modules/aurelia-loader-nodejs/dist/commonjs/aurelia-loader-nodejs.js:286
    module:  template-registry-entry!components/field/common/datepicker.html

Modified NodeJsLoader node_modules\aurelia-loader-nodejs\dist\commonjs\aurelia-loader-nodejs.js

NodeJsLoader.prototype.loadModule = function (moduleId) {
        return __awaiter(this, void 0, void 0, function () {
            var _this = this;
            var existing, beingLoaded, moduleExports;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        existing = this.moduleRegistry[moduleId];
                        if (existing) {
                            return [2 /*return*/, existing];
                        }
                        beingLoaded = this.modulesBeingLoaded.get(moduleId);
                        if (beingLoaded) {
                            return [2 /*return*/, beingLoaded];
                        }
                        beingLoaded = this._import(moduleId).catch(function (e) {
                            _this.modulesBeingLoaded.delete(moduleId);
                            throw e;
                        });
                        this.modulesBeingLoaded.set(moduleId, beingLoaded);
                        return [4 /*yield*/, beingLoaded];
                    case 1:
                        moduleExports = _a.sent();
                        this.moduleRegistry[moduleId] = ensureOriginOnExports(moduleExports, moduleId);

                        console.warn('module: ', moduleId);

                        this.modulesBeingLoaded.delete(moduleId);
                        return [2 /*return*/, moduleExports];
                }
            });
        });
    };

Related issues: https://github.com/aurelia/templating-resources/issues/64#issuecomment-147269442

Sayan751 commented 5 years ago

It is difficult to suggest any workaround without seeing the code for unit testing. However, there are a few things you can try as workaround.

  1. Switch to manuallyHandleLifecycle to have a better control on the lifecycle, if needed.
  2. Wait explicitly for the stuffs to get completed, especially when you can't wait on the promise. A simple wait function can be as follows:
    function wait(timeInMs = 1000) {
        return new Promise((resolve) => { setTimeout(() => { resolve(); }, timeInMs); });
    }

    Then, you can use it as follows:

    await component.bind(...);
    await wait();

    Hope this helps.

silbinarywolf commented 5 years ago

Thanks for the advice! I opted to go with 1.

My current solution (which I literally just created a few mins ago, so might not be robust) is to do something like this.

Im not sure why simply using a few promises gives it enough time and I might want to add some code to guarantee that the was mounted, but this will do for now.

function workaroundComposeMountIssue(): Promise<void> {
    return component.manuallyHandleLifecycle().create(bootstrap)
      .then(() => (component as any).bind())
      .then(() => {
        component.attached()
      });
};

it('trying to get tests working', (done) => {
    workaroundComposeMountIssue().then(() => {
        expect(component.viewModel.value).toBe('2018-09-19');
        done();
    })
});
Sayan751 commented 5 years ago

@silbinarywolf By doing so, you are basically telling jasmine to wait. Or in other words, your assertions are only executed once the previous promises (involving the lifecycle hooks of the custom element) are resolved.

Alexander-Taran commented 5 years ago

@silbinarywolf can this be closed?

silbinarywolf commented 5 years ago

Sure. I'll close it.