ViewModel компонента доступа к модульному тестированию Aurelia

Я провожу модульное тестирование одного из своих компонентов в проекте Aurelia. Я хотел бы получить доступ к viewModel моего компонента в моем модульном тесте, но пока мне не повезло.

Я следовал примеру, доступному по адресу https://aurelia.io/docs/testing/components#manually-handling-lifecycle но я продолжаю получать component.viewModel is undefined.

Вот модульный тест:

describe.only('some basic tests', function() {
    let component, user;

    before(() => {
        user = new User({ id: 100, first_name: "Bob", last_name: "Schmoe", email: '[email protected]'});
        user.save();
    });

    beforeEach( () => {
        component = StageComponent
            .withResources('modules/users/user')
            .inView('<user></user>')
            .boundTo( user );
    });

    it('check for ', () => {
        return component.create(bootstrap)
            .then(() => {
                expect(2).to.equal(2);
                return component.viewModel.activate({user: user});
            });

    });

    it('can manually handle lifecycle', () => {

        return component.manuallyHandleLifecycle().create(bootstrap)
            .then(() => component.bind({user: user}))
            .then(() => component.attached())
            .then(() => component.unbind() )
            .then(() => {
                expect(component.viewModel.name).toBe(null);
                return Promise.resolve(true);
        });
    });

    afterEach( () => {
        component.dispose();
    });
});

Вот ошибка, которую я получаю:

1) my aurelia tests
       can manually handle lifecycle:
     TypeError: Cannot read property 'name' of undefined

Вот строка, которая определяет viewModel для объекта component, но только если установлено aurelia.root.controllers.length. Я не уверен, как установить контроллеры в моем коде aurelia и нужно ли мне это делать вообще.

Я предполагаю, что мой вопрос: как мне получить доступ к модели представления компонента в моих модульных тестах?


person user4426017    schedule 24.10.2018    source источник


Ответы (2)


Редактировать № 2:

Я также хотел бы отметить, что ваш собственный ответ по сути является тем же решением, что и то, которое я впервые предложил в комментариях. Это эквивалентно непосредственному созданию экземпляра вашей модели представления без проверки того, действительно ли работает компонент.

Редактировать:

Я попробовал это локально с настройкой karma+webpack+mocha (поскольку webpack в настоящее время является популярным выбором), и было несколько предостережений, чтобы это работало хорошо. Я не уверен, какова остальная часть вашей настройки, поэтому я не могу точно сказать вам, где была ошибка (вероятно, я мог бы указать на это, если бы вы рассказали мне больше о своей настройке).

В любом случае, вот рабочая установка с karma+webpack+mocha, которая правильно проверяет привязку и рендеринг:

https://github.com/fkleuver/aurelia-karma-webpack-testing

Код теста:

import './setup';
import { Greeter } from './../src/greeter';
import { bootstrap } from 'aurelia-bootstrapper';
import { StageComponent, ComponentTester } from 'aurelia-testing';
import { PLATFORM } from 'aurelia-framework';
import { assert } from 'chai';

describe('Greeter', () => {
  let el: HTMLElement;
  let tester: ComponentTester;
  let sut: Greeter;

  beforeEach(async () => {
    tester = StageComponent
      .withResources(PLATFORM.moduleName('greeter'))
      .inView(`<greeter name.bind="name"></greeter>`)
      .manuallyHandleLifecycle();

    await tester.create(bootstrap);
    el = <HTMLElement>tester.element;
    sut = tester.viewModel;
  });

  it('binds correctly', async () => {
    await tester.bind({ name: 'Bob' });

    assert.equal(sut.name, 'Bob');
  });

  it('renders correctly', async () => {
    await tester.bind({ name: 'Bob' });
    await tester.attached();

    assert.equal(el.innerText.trim(), 'Hello, Bob!');
  });
});

greeter.html

<template>
  Hello, ${name}!
</template>

greeter.ts

import { bindable } from 'aurelia-framework';

export class Greeter {
  @bindable()
  public name: string;
}

setup.ts

import 'aurelia-polyfills';
import 'aurelia-loader-webpack';
import { initialize } from 'aurelia-pal-browser';

initialize();

karma.conf.js

const { AureliaPlugin } = require('aurelia-webpack-plugin');
const { resolve } = require('path');

module.exports = function configure(config) {
  const options = {
    frameworks: ['source-map-support', 'mocha'],
    files: ['test/**/*.ts'],
    preprocessors: { ['test/**/*.ts']: ['webpack', 'sourcemap'] },
    webpack: {
      mode: 'development',
      entry: { setup: './test/setup.ts' },
      resolve: {
        extensions: ['.ts', '.js'],
        modules: [
          resolve(__dirname, 'src'),
          resolve(__dirname, 'node_modules')
        ]
      },
      devtool: 'inline-source-map',
      module: {
        rules: [{
          test: /\.html$/i,
          loader: 'html-loader'
        }, {
          test: /\.ts$/i,
          loader: 'ts-loader',
          exclude: /node_modules/
        }]
      },
      plugins: [new AureliaPlugin()]
    },
    singleRun: false,
    colors: true,
    logLevel: config.browsers && config.browsers[0] === 'ChromeDebugging' ? config.LOG_DEBUG : config.LOG_INFO, // for troubleshooting mode
    mime: { 'text/x-typescript': ['ts'] },
    webpackMiddleware: { stats: 'errors-only' },
    reporters: ['mocha'],
    browsers: config.browsers || ['ChromeHeadless'],
    customLaunchers: {
      ChromeDebugging: {
        base: 'Chrome',
        flags: [ '--remote-debugging-port=9333' ]
      }
    }
  };

  config.set(options);
};

tsconfig.json

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "importHelpers": true,
    "lib": ["es2018", "dom"],
    "module": "esnext",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es2018"
  },
  "include": ["src"]
}

package.json

{
  "scripts": {
    "test": "karma start --browsers=ChromeHeadless"
  },
  "dependencies": {
    "aurelia-bootstrapper": "^2.3.0",
    "aurelia-loader-webpack": "^2.2.1"
  },
  "devDependencies": {
    "@types/chai": "^4.1.6",
    "@types/mocha": "^5.2.5",
    "@types/node": "^10.12.0",
    "aurelia-testing": "^1.0.0",
    "aurelia-webpack-plugin": "^3.0.0",
    "chai": "^4.2.0",
    "html-loader": "^0.5.5",
    "karma": "^3.1.1",
    "karma-chrome-launcher": "^2.2.0",
    "karma-mocha": "^1.3.0",
    "karma-mocha-reporter": "^2.2.5",
    "karma-source-map-support": "^1.3.0",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-webpack": "^3.0.5",
    "mocha": "^5.2.0",
    "path": "^0.12.7",
    "ts-loader": "^5.2.2",
    "typescript": "^3.1.3",
    "webpack": "^4.23.1",
    "webpack-dev-server": "^3.1.10"
  }
}

Оригинальный ответ

Если вы вручную выполняете жизненный цикл, вам нужно самостоятельно передать ViewModel, к которому он может привязываться :)

Я точно не помню, что, строго говоря, нужно, поэтому я совершенно уверен, что есть некоторая избыточность (например, один из двух переданных контекстов привязки не должен быть необходим). Но это общая идея:

const view = "<div>${msg}</div>";
const bindingContext = { msg: "foo" };
StageComponent
  .withResources(resources/*optional*/)
  .inView(view)
  .boundTo(bindingContext)
  .manuallyHandleLifecycle()
  .create(bootstrap)
  .then(component => {
    component.bind(bindingContext);
  }
  .then(component => {
    component.attached();
  }
  .then(component => {
    expect(component.host.textContent).toEqual("foo");
  }
  .then(component => {
    bindingContext.msg = "bar";
  }
  .then(component => {
    expect(component.host.textContent).toEqual("bar");
  };

Само собой разумеется, поскольку вы сами создаете модель представления (переменная bindingContext в этом примере), вы можете просто получить доступ к объявленной вами переменной.

person Fred Kleuver    schedule 25.10.2018
comment
Я прибегнул к ручному жизненному циклу, потому что с обычным тоже ничего не получалось, т.е. я получал ту же ошибку. Да, если я воссоздам viewModel в своем тестовом файле, тогда проблем не будет. Но мне не нужно воссоздавать мою модель представления в тестовом файле. Я должен иметь возможность получить доступ к моей фактической модели представления в моем тестовом коде. Также мой viewModel - это не просто объект данных. Он имеет несколько методов, которые выполняют асинхронные вызовы API к серверной части для извлечения и изменения данных. Для меня действительно не идеально дублировать мою модель представления в моем тестовом коде. - person user4426017; 25.10.2018
comment
Нет необходимости дублировать вашу модель представления. Вам просто нужно передать его экземпляр тестировщику компонентов. Я не могу угадать все это, основываясь на коде, который вы разместили. Все, что я вижу, это то, что вы не передаете тестеру некоторые части информации, которые ему нужны. Если вы передаете эту информацию, вы должны включить этот код в свой вопрос, чтобы люди могли вам помочь. - person Fred Kleuver; 25.10.2018
comment
Чтобы ответить на ваш вопрос напрямую: вам может понадобиться использовать component.bindingContext вместо .viewModel, но если ни один из них не существует, вы не даете ему свою модель представления, так как же он может это понять? - person Fred Kleuver; 25.10.2018
comment
Я включил дополнительный код в свой фрагмент кода выше. У меня есть доступ к объекту пользователя, но не к модели представления. И это мой вопрос, как мне включить мою модель представления или получить доступ к моей модели представления в моем модульном тесте? - person user4426017; 25.10.2018
comment
Пользовательский объект является ViewModel. К чему бы вы его ни привязали, это ViewModel. Вы также должны иметь доступ к нему через component.bindingContext, как я сказал в своем предыдущем комментарии. - person Fred Kleuver; 26.10.2018
comment
Пользовательский объект может быть моделью представления, но модель представления является файлом javascript при создании компонента. См. aurelia.io/docs/fundamentals/components#creating-a-component Прочтите последнее предложение второго абзаца. Он ссылается на файл javascript как на модель представления и на файл HTML как на представление. Также обратите внимание, что в viewModel (файле JS) есть методы, которые я хотел бы протестировать. Так что просто пользовательский объект - это не то, что я ищу. - person user4426017; 26.10.2018
comment
Когда вы создаете пользовательский элемент по соглашению, представление — это файл html, а модель представления — это действительно файл javascript. Ну, на самом деле это не файл javascript, а экземпляр класса, который вы объявили в файле javascript. Это все еще класс, который вы объявили, и это все еще модель представления. У него нет специальных методов (в этом суть фреймворка). Если вы ищете методы жизненного цикла, вы, вероятно, ищете View / ViewSlot / Controller. Эти методы выставляются/инкапсулируются тестировщиком компонентов. Вы уже звоните им.. - person Fred Kleuver; 26.10.2018
comment
Неважно, что я сказал выше, теперь я понимаю, что вы ищете экземпляр модели представления пользовательского элемента user, когда вы передаете контекст привязки приложения/родителя для привязки. Я еще раз проверю сегодня, когда буду дома, и вернусь к вам - person Fred Kleuver; 26.10.2018
comment
Мне кажется, то, что вы делаете, правильно. Я сделал некоторую отладку и столкнулся с той же проблемой. Я предполагаю, что вы используете karma-webpack? Вам нужно использовать AureliaPlugin внутри конфигурации кармы. Без этого он не сможет загрузить части aurelia (это произойдет автоматически), и экземпляр Aurelia не запустится правильно. Однако я столкнулся с другой ошибкой плагина, которая на первый взгляд кажется ошибкой в ​​плагине karma-webpack. Я пытаюсь решить эту проблему прямо сейчас и дам вам знать, когда у меня будет - person Fred Kleuver; 26.10.2018

Чтобы заставить его работать, мне пришлось использовать Container:

import { UserCard } from '../../src/modules/users/user-card';
import { Container } from 'aurelia-dependency-injection';


describe.only('some basic tests', function() {
    let component, user;

    before(() => {
        user = new User({ id: 100, first_name: "Bob", last_name: "Schmoe", email: '[email protected]'});
        user.save();
    });

    beforeEach(() => {
        container = new Container();
        userCard = container.get( UserCard );
        component = StageComponent
            .withResources('modules/users/user-card')
            .inView('<user-card></user-card>')
            .boundTo( user );
    });

    it('check for ', () => {
        return component.create(bootstrap)
            .then(() => {
                expect(2).to.equal(2);
                return userCard.activate({user: user});
            });
    });
});
person user4426017    schedule 26.10.2018
comment
Извините, но это неправильно. Что вы эффективно делаете, так это заставляете контейнер регистрировать и создавать экземпляр синглтона вашего класса UserCard, который затем повторно использует фреймворк. Обычно пользовательские модели представлений элементов регистрируются как временные. Вы в основном ничего не тестируете/проверяете в этом тесте, кроме того факта, что контейнер может создать экземпляр вашей модели представления, вызвав свой конструктор. Вы проверили html? - person Fred Kleuver; 27.10.2018
comment
Пожалуйста, смотрите последнее обновление моего ответа. Попробуйте это репо, и вы поймете, что я имею в виду. - person Fred Kleuver; 27.10.2018