Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(ivy): use TestBed for render3 component tests #30282

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
92 changes: 91 additions & 1 deletion packages/core/test/acceptance/component_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Component, ComponentFactoryResolver, ComponentRef, InjectionToken, NgModule, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {Component, ComponentFactoryResolver, ComponentRef, InjectionToken, NgModule, OnDestroy, Type, ViewChild, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';

Expand Down Expand Up @@ -88,4 +88,94 @@ describe('component', () => {
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('foo|bar');
});

// TODO: add tests with Native once tests run in real browser (domino doesn't support shadow root)
describe('encapsulation', () => {
@Component({
selector: 'wrapper',
encapsulation: ViewEncapsulation.None,
template: `<encapsulated></encapsulated>`
})
class WrapperComponent {
}

@Component({
selector: 'encapsulated',
encapsulation: ViewEncapsulation.Emulated,
// styles array must contain a value (even empty) to trigger `ViewEncapsulation.Emulated`
styles: [``],
template: `foo<leaf></leaf>`
})
class EncapsulatedComponent {
}

@Component(
{selector: 'leaf', encapsulation: ViewEncapsulation.None, template: `<span>bar</span>`})
class LeafComponent {
}

beforeEach(() => {
TestBed.configureTestingModule(
{declarations: [WrapperComponent, EncapsulatedComponent, LeafComponent]});
});

it('should encapsulate children, but not host nor grand children', () => {
const fixture = TestBed.createComponent(WrapperComponent);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML)
.toMatch(
/<encapsulated _nghost-[a-z\-]+(\d+)="">foo<leaf _ngcontent-[a-z\-]+\1=""><span>bar<\/span><\/leaf><\/encapsulated>/);
});

it('should encapsulate host', () => {
const fixture = TestBed.createComponent(EncapsulatedComponent);
fixture.detectChanges();
const html = fixture.nativeElement.outerHTML;
const match = html.match(/_nghost-([a-z\-]+\d+)/);
expect(match).toBeDefined();
expect(html).toMatch(new RegExp(`<leaf _ngcontent-${match[1]}=""><span>bar</span></leaf>`));
});

it('should encapsulate host and children with different attributes', () => {
// styles array must contain a value (even empty) to trigger `ViewEncapsulation.Emulated`
TestBed.overrideComponent(
LeafComponent, {set: {encapsulation: ViewEncapsulation.Emulated, styles: [``]}});
const fixture = TestBed.createComponent(EncapsulatedComponent);
fixture.detectChanges();
const html = fixture.nativeElement.outerHTML;
const match = html.match(/_nghost-([a-z\-]+\d+)/g);
expect(match).toBeDefined();
expect(match.length).toEqual(2);
expect(html).toMatch(
`<leaf ${match[0].replace('_nghost', '_ngcontent')}="" ${match[1]}=""><span ${match[1].replace('_nghost', '_ngcontent')}="">bar</span></leaf></div>`);
});
});

describe('view destruction', () => {
it('should invoke onDestroy when directly destroying a root view', () => {
let wasOnDestroyCalled = false;

@Component({selector: 'comp-with-destroy', template: ``})
class ComponentWithOnDestroy implements OnDestroy {
ngOnDestroy() { wasOnDestroyCalled = true; }
}

// This test asserts that the view tree is set up correctly based on the knowledge that this
// tree is used during view destruction. If the child view is not correctly attached as a
// child of the root view, then the onDestroy hook on the child view will never be called
// when the view tree is torn down following the destruction of that root view.
@Component({selector: `test-app`, template: `<comp-with-destroy></comp-with-destroy>`})
class TestApp {
}

TestBed.configureTestingModule({declarations: [ComponentWithOnDestroy, TestApp]});
const fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();
fixture.destroy();
expect(wasOnDestroyCalled)
.toBe(
true,
'Expected component onDestroy method to be called when its parent view is destroyed');
});
});
});
166 changes: 2 additions & 164 deletions packages/core/test/render3/component_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/

import {InjectionToken, ViewEncapsulation, ɵɵdefineInjectable, ɵɵdefineInjector} from '../../src/core';
import {ViewEncapsulation, ɵɵdefineInjectable, ɵɵdefineInjector} from '../../src/core';
import {createInjector} from '../../src/di/r3_injector';
import {AttributeMarker, ComponentFactory, LifecycleHooksFeature, getRenderedText, markDirty, ɵɵProvidersFeature, ɵɵdefineComponent, ɵɵdirectiveInject, ɵɵtemplate} from '../../src/render3/index';
import {AttributeMarker, ComponentFactory, LifecycleHooksFeature, getRenderedText, markDirty, ɵɵdefineComponent, ɵɵdirectiveInject, ɵɵtemplate} from '../../src/render3/index';
import {tick, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵnextContext, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all';
import {ComponentDef, RenderFlags} from '../../src/render3/interfaces/definition';

import {NgIf} from './common_with_def';
import {getRendererFactory2} from './imported_renderer2';
import {ComponentFixture, MockRendererFactory, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame, toHtml} from './render_util';

describe('component', () => {
Expand Down Expand Up @@ -213,7 +212,6 @@ it('should not invoke renderer destroy method for embedded views', () => {
});

describe('component with a container', () => {

function showItems(rf: RenderFlags, ctx: {items: string[]}) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
Expand Down Expand Up @@ -284,127 +282,6 @@ describe('component with a container', () => {
ctx.items = [...ctx.items, 'b'];
expect(renderToHtml(template, ctx, 1, 1, defs)).toEqual('<wrapper>ab</wrapper>');
});

});

// TODO: add tests with Native once tests are run in real browser (domino doesn't support shadow
// root)
describe('encapsulation', () => {
class WrapperComponent {
static ngComponentDef = ɵɵdefineComponent({
type: WrapperComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['wrapper']],
consts: 1,
vars: 0,
template: function(rf: RenderFlags, ctx: WrapperComponent) {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'encapsulated');
}
},
factory: () => new WrapperComponent,
directives: () => [EncapsulatedComponent]
});
}

class EncapsulatedComponent {
static ngComponentDef = ɵɵdefineComponent({
type: EncapsulatedComponent,
selectors: [['encapsulated']],
consts: 2,
vars: 0,
template: function(rf: RenderFlags, ctx: EncapsulatedComponent) {
if (rf & RenderFlags.Create) {
ɵɵtext(0, 'foo');
ɵɵelement(1, 'leaf');
}
},
factory: () => new EncapsulatedComponent,
encapsulation: ViewEncapsulation.Emulated,
styles: [],
data: {},
directives: () => [LeafComponent]
});
}

class LeafComponent {
static ngComponentDef = ɵɵdefineComponent({
type: LeafComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['leaf']],
consts: 2,
vars: 0,
template: function(rf: RenderFlags, ctx: LeafComponent) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'span');
{ ɵɵtext(1, 'bar'); }
ɵɵelementEnd();
}
},
factory: () => new LeafComponent,
});
}

it('should encapsulate children, but not host nor grand children', () => {
renderComponent(WrapperComponent, {rendererFactory: getRendererFactory2(document)});
expect(containerEl.outerHTML)
.toMatch(
/<div host=""><encapsulated _nghost-[a-z]+-c(\d+)="">foo<leaf _ngcontent-[a-z]+-c\1=""><span>bar<\/span><\/leaf><\/encapsulated><\/div>/);
});

it('should encapsulate host', () => {
renderComponent(EncapsulatedComponent, {rendererFactory: getRendererFactory2(document)});
expect(containerEl.outerHTML)
.toMatch(
/<div host="" _nghost-[a-z]+-c(\d+)="">foo<leaf _ngcontent-[a-z]+-c\1=""><span>bar<\/span><\/leaf><\/div>/);
});

it('should encapsulate host and children with different attributes', () => {
class WrapperComponentWith {
static ngComponentDef = ɵɵdefineComponent({
type: WrapperComponentWith,
selectors: [['wrapper']],
consts: 1,
vars: 0,
template: function(rf: RenderFlags, ctx: WrapperComponentWith) {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'leaf');
}
},
factory: () => new WrapperComponentWith,
encapsulation: ViewEncapsulation.Emulated,
styles: [],
data: {},
directives: () => [LeafComponentwith]
});
}

class LeafComponentwith {
static ngComponentDef = ɵɵdefineComponent({
type: LeafComponentwith,
selectors: [['leaf']],
consts: 2,
vars: 0,
template: function(rf: RenderFlags, ctx: LeafComponentwith) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'span');
{ ɵɵtext(1, 'bar'); }
ɵɵelementEnd();
}
},
factory: () => new LeafComponentwith,
encapsulation: ViewEncapsulation.Emulated,
styles: [],
data: {},
});
}

renderComponent(WrapperComponentWith, {rendererFactory: getRendererFactory2(document)});
expect(containerEl.outerHTML)
.toMatch(
/<div host="" _nghost-[a-z]+-c(\d+)=""><leaf _ngcontent-[a-z]+-c\1="" _nghost-[a-z]+-c(\d+)=""><span _ngcontent-[a-z]+-c\2="">bar<\/span><\/leaf><\/div>/);
});

});

describe('recursive components', () => {
Expand Down Expand Up @@ -671,42 +548,3 @@ describe('recursive components', () => {
});

});

describe('view destruction', () => {
it('should invoke onDestroy when directly destroying a root view', () => {
let wasOnDestroyCalled = false;

class ComponentWithOnDestroy {
static ngComponentDef = ɵɵdefineComponent({
selectors: [['comp-with-destroy']],
type: ComponentWithOnDestroy,
consts: 0,
vars: 0,
factory: () => new ComponentWithOnDestroy(),
template: (rf: any, ctx: any) => {},
});

ngOnDestroy() { wasOnDestroyCalled = true; }
}

// This test asserts that the view tree is set up correctly based on the knowledge that this
// tree is used during view destruction. If the child view is not correctly attached as a
// child of the root view, then the onDestroy hook on the child view will never be called
// when the view tree is torn down following the destruction of that root view.
const ComponentWithChildOnDestroy = createComponent('test-app', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'comp-with-destroy');
}
}, 1, 0, [ComponentWithOnDestroy], [], null, [], []);

const fixture = new ComponentFixture(ComponentWithChildOnDestroy);
fixture.update();

fixture.destroy();
expect(wasOnDestroyCalled)
.toBe(
true,
'Expected component onDestroy method to be called when its parent view is destroyed');
});

});