如何在 Angular 中测试嵌入的内容?

Max*_*Art 5 javascript testing unit-testing angular

在测试具有嵌入槽的 Angular 组件时<ng-content>,我们没有明确的方法来检查嵌入的内容是否按预期放置在组件内。例如:

// base-button.component.ts
@Component({
  selector: 'base-button',
  template: `<button [type]="type">
    <ng-content></ng-content>
  </button>`,
})
export class BaseButtonComponent {
  @Input() type = 'button';
}
Run Code Online (Sandbox Code Playgroud)

基本上,在 spec 文件中创建组件实例时,我们这样做:

// base-button.component.spec.ts
it('should reflect the `type` property into the "type" attribute of the button', () => {
  const fixture = TestBed.createComponent(BaseButtonComponent);
  fixture.detectChanges();

  const { componentInstance, nativeElement } = fixture;
  componentInstance.type = 'reset';

  const button = nativeElement.querySelector('button');
  expect(button.type === 'reset');
});
Run Code Online (Sandbox Code Playgroud)

我们可以对组件的每个属性和方法都这样做,但是嵌入的内容呢?一种解决方法是创建一个用于测试目的的主机组件:

// base-button.component.spec.ts
...
@Component({
  template: `<base-button>Foo bar</base-button>`
})
export class BaseButtonHostComponent {}
...

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ BaseButtonComponent, BaseButtonHostComponent ]
    })
    .compileComponents();
  }));

  it('should transclude the content correctly', () => {
    const hostFixture = TestBed.createComponent(BaseButtonHostComponent);
    hostFixture.detectChanges();
    const button = hostFixture.nativeElement.querySelector('button');
    expect(button.textContent === 'Foo bar');
  });
...
Run Code Online (Sandbox Code Playgroud)

但是,正如您可以想象的那样,这相当不方便,还因为必须为每个包含嵌入内容的组件以及可能<ng-content>为其模板中的每个元素执行此操作。有没有另一种方法可以做到这一点?

Max*_*Art 3

确实有一种相当晦涩的方法来做到这一点。基本上,TestBed.createComponent调用组件的工厂create方法,该方法还支持将可投影 DOM 节点插入到嵌入槽中。

// @angular/core/testing.js
createComponent(component) {
  ...
  const componentFactory = this._compiler.getComponentFactory(component);
  ...
  const componentRef = componentFactory.create(Injector.NULL, [], `#${rootElId}`, this._moduleRef);
  ...
}
Run Code Online (Sandbox Code Playgroud)

我们必须做同样的事情,技巧如下:

// base-button.component.spec.ts
describe('BaseButtonComponent', () => {
  let factory: ComponentFactory<BaseButtonComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ BaseButtonComponent ]
    })
    .overrideModule(BrowserDynamicTestingModule, {
      set: {
        entryComponents: [ BaseButtonComponent ]
      }
    })
    .compileComponents();

    const resolver = <ComponentFactoryResolver>TestBed.get(ComponentFactoryResolver, null);
    factory = resolver.resolveComponentFactory(BaseButtonComponent);
  }));

  it('should transclude the provided nodes into the button', () => {
    const tnode = document.createTextNode('Foo bar');
    const componentRef = factory.create(Injector.NULL, [[ tnode ]]);
    const button = componentRef.location.nativeElement.querySelector('button');
    expect(button.textContent === 'Foo bar');
  });
});
Run Code Online (Sandbox Code Playgroud)

TestBed.get允许我们检索ComponentFactoryResolver服务。但是,为了检索组件的工厂,组件的类必须列在模块的entryComponents 属性中。所讨论的模块BrowserDynamicTestingModule公开TestBed了一种方便的方法来更改其属性。

一旦你拥有了工厂,技巧就完成了。唯一烦人的部分是手动生成所有可投影节点,因此您可以为此创建一个实用函数:

function createComponentWithContents(factory, ...contents) {
  const template = document.createElement('template');
  const projectableNodes = contents.map(html => {
    template.innerHTML = html;
    return [ ...template.content.childNodes ];
  });
  return factory.create(Injector.NULL, projectableNodes);
}

const componentRef = createComponentWithContents(factory, '<i class="fa fa-star"></i> Win!');
Run Code Online (Sandbox Code Playgroud)

遗憾的是TestBed.createComponent不允许立即这样做。