How to test transcluded content in Angular? - javascript

While testing an Angular component that has transclusion slots with <ng-content>, we have no
explicit means to check if the transcluded content is placed as intended inside the component.
For example:
// base-button.component.ts
#Component({
selector: 'base-button',
template: `<button [type]="type">
<ng-content></ng-content>
</button>`,
})
export class BaseButtonComponent {
#Input() type = 'button';
}
Basically, when creating a component instance in the spec file, we do this:
// 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');
});
We can do this for every property and method of the component, but what about the
transcluded content? A workaround would be creating a host component for test purposes:
// 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');
});
...
But, as you could imagine, this is rather inconvenient, also because this has to be done
for every component with transcluded content, and possibly for every <ng-content> element
in its template. Is there another way to do this?

There's indeed a rather obscure way to do it. Basically, TestBed.createComponent invokes
the component's factory create method, which also supports projectable DOM nodes to be
inserted into transclusion slots.
// #angular/core/testing.js
createComponent(component) {
...
const componentFactory = this._compiler.getComponentFactory(component);
...
const componentRef = componentFactory.create(Injector.NULL, [], `#${rootElId}`, this._moduleRef);
...
}
We have to do the same, and here's the trick:
// 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');
});
});
TestBed.get allows us to retrieve the ComponentFactoryResolver service. In order to retrieve
the component's factory, though, the component's class must be listed in the module's entryComponents
property. The module in question is BrowserDynamicTestingModule and TestBed exposes a handy
method to alter its properties.
Once you have the factory, the trick is served. The only annoying part is generating all the
projectable nodes by hand, so you can create a utility function for that:
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!');
It's a shame that TestBed.createComponent doesn't allow to do that right away.

Related

In unit test: How to override an NGRX selector which is created by entityAdapter.getSelectors()

Assume that our app has a books page.
We are using: Angular, NGRX, jest.
Some lines of code to give a context (see actual problem below):
Interfaces of the books page's state:
export interface Book {
bookID: string;
whatever: string;
}
export interface Books extends EntityState<Book> {
error: boolean;
loaded: boolean;
}
export interface BooksFeature extends RootState {
books: Books;
//...
}
Added to ngrx store as feature:
#NgModule({
imports: [
StoreModule.forFeature('books', booksReducer),
//...
]
ngrx entityAdapter is created:
export const booksAdapter = createEntityAdapter<Book>({
selectId: (book: Book): string => book.bookID,
});
Create booksSelector from booksAdapter's selectAll
const { selectAll, selectTotal } = booksAdapter.getSelectors((state: BooksFeature) => state.books);
export const booksSelector = selectAll;
Assign booksSelector to the component property: books$
public books$ = this.store.pipe(select(booksSelector));
Then the books$ observable is used for many things (eg. <div *ngIf="(books$ | async).length">, etc...).
The goal: Assume that I would like to unit test separately if the books$ observable has always the same value as what the booksSelector broadcasts.
Normally I would do the following in the component's books.component.spec.ts file:
General setup for component test:
//...
describe('BooksComponent', () => {
let spectator: Spectator<BooksComponent>
let store: MockStore;
const initialState: RootState = {
//...
books: {
ids: [],
entities: {},
error: false,
loaded: false
}
};
const testBook: Book = {
bookID: 'bookid_1',
whatever: 'whatever'
};
const createComponent = createComponentFactory({
component: BooksComponent,
providers: [
provideMockStore({ initialState })
],
imports: [
StoreModule.forRoot({}),
detectChanges: false
]
});
beforeEach(() => {
spectator = createComponent();
store = spectator.inject(MockStore);
});
afterEach(() => {
jest.clearAllMocks();
});
//...
And the important part:
//...
describe('books$', () => {
it('books$ should have the same value as what booksSelector gives', () => {
const testBooks: Book[] = [testBook];
const expected = cold('a', { a: testBooks });
const mockBooksSelector = store.overrideSelector(booksSelector, testBooks);
expect(spectator.component.books$).toBeObservable(expected);
});
//... Then I would like to use the mockBooksSelector.setResult(...) method too for other test cases
});
//...
The problem with this is that the MockStore's overrideSelector method expects a Selector as first parameter, but the entityAdapter's getSelectors method returns with a selectAll method that has a different type.
Please let me know how could I replace this test with a proper solution!
Please keep in mind, that the problem is simplified to keep it focused and I'm not looking for solutions like these:
Test instead if the store.pipe is called with the proper select.
Change the state manually in order to have the wished value given by booksSelector.
Solutions that change things not only in the .spec file. (I mean, if it's really inevitable then OK)
Thx!
You can cast booksSelector to any type:
const mockBooksSelector = store.overrideSelector(booksSelector as any, testBooks)
You need to use feature selector as input to getSelectors:
const selectBookFeatureState =
createFeatureSelector<BooksFeature>('books');
const { selectAll, selectTotal } = booksAdapter.getSelectors(selectBookFeatureState)
and if that doesn't work you can try creating selectors like this:
const selectAllBooks = createSelector(
selectBookFeatureState,
selectAll
)
const selectTotalBooks = createSelector(
selectBookFeatureState,
selectTotal
)
and use selectAllBooks instead selectAll and selectTotalBooks instead selectTotal

How do I update component variables in Angular unit tests?

I am having a problem where I set the "headerButtons" and "contractLoaded" component variables in my test but it does not seem to change the values. If I console out or use a debugger in the component as the test runs the variables stay as initially defined (undefined and false).
I have tried lots of different combinations but always the same result.
headerButtons is an #Input
let component: HeaderBannerComponent;
let fixture: ComponentFixture<HeaderBannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [HeaderBannerComponent] });
fixture = TestBed.createComponent(HeaderBannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
...
it('should display the correct amount of header buttons', () => {
const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
fixture.detectChanges();
expect(debugEl.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
fixture.detectChanges();
expect(
debugEl.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});
Component :
import { Component, Input, OnInit } from '#angular/core';
import { KeyValue } from '#angular/common';
import { ContractEventService } from 'src/app/core/services/contract-event.service';
#Component({
selector: 'app-header-banner',
templateUrl: './header-banner.component.html',
styleUrls: ['./header-banner.component.css'],
})
export class HeaderBannerComponent implements OnInit {
menubar: Map<string, string> = new Map<string, string>();
contractLoaded: boolean = false;
#Input() headerTitle: any;
#Input() headerButtons: string;
constructor(private contractEventService: ContractEventService) {}
ngOnInit(): void {
if (this.headerButtons === 'full') {
this.contractEventService.getContractLoaded().subscribe(
(rs) => {
this.contractLoaded = rs;
this.setButtons();
},
(err) => {
console.warn('failed to get contractLoaded status', err);
}
);
}
this.setButtons();
}
setButtons(): void {
this.menubar = new Map<string, string>();
this.menubar.set('Home', '/iforis/main.do');
if (this.headerButtons === 'full' && this.contractLoaded) {
this.menubar.set('Contacts', '/iforis/s/contacts/');
this.menubar.set('Notes', '/iforis/s/notes/');
}
if (this.headerButtons === 'full' && !this.contractLoaded) {
this.menubar.delete('Contacts');
this.menubar.delete('Notes');
}
this.menubar.set('Exit', '/iforis/exit.do');
}
originalOrder = (
a: KeyValue<string, string>,
b: KeyValue<string, string>
): number => {
return 0;
};
}
Template:
<div class="header-banner box-shadow">
<div class="header-banner__title">
<span id="headerTitleText" class="header-banner__text">
{{ headerTitle }}
</span>
</div>
<div class="header-banner__logo">
<img src="assets/DAFM_Logo_2018.png" />
</div>
<div class="header-banner__btn-container">
<div
*ngFor="
let button of menubar | keyvalue: originalOrder;
let first = first;
let last = last
"
[ngClass]="[
'header-banner__btn',
first ? 'header-banner__btn--first' : '',
last ? 'header-banner__btn--last' : ''
]"
>
<a href="{{ button.value }}" class="header-banner__btn-link">
{{ button.key }}
</a>
</div>
</div>
</div>
That is strange. Can you show the HTML and component typescript as well?
I think I have an idea though about the issue you could be facing.
it('should display the correct amount of header buttons', () => {
// get rid of this variable
// const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(
fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});
The issue is the debugEl. It becomes stale after you change component variables so you always need a new debugEl after changing variables. I think this is most likely the issue.
====== Edit ======
Maybe we should mock ContractEventService instead of providing the real one.
Try this:
let component: HeaderBannerComponent;
let fixture: ComponentFixture<HeaderBannerComponent>;
let mockContractEventService: jasmine.SpyObj<ContractEventService>;
beforeEach(() => {
// the first string argument is just an identifier and is optional
// the second array of strings are public methods that we need to mock
mockContractEventService = jasmine.createSpyObj<ContractEventService>('ContractEventService', ['getContractLoaded']);
TestBed.configureTestingModule({
declarations: [HeaderBannerComponent],
// provide the mock when the component asks for the real one
providers: [{ provide: ContractEventService, useValue: mockContractService }]
});
fixture = TestBed.createComponent(HeaderBannerComponent);
component = fixture.componentInstance;
// formatting is off but return a fake value for getContractLoaded
mockContractEventService.getContractLoaded.and.returnValue(of(true));
// the first fixture.detectChanges() is when ngOnInit is called
fixture.detectChanges();
});
it('should display the correct amount of header buttons', () => {
// get rid of this variable
// const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
// The issue was that we are not calling setButtons
// manually call setButtons
component.setButtons();
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
// manually call setButtons
component.setButtons();
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(
fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});

Angular 6 Karma/Jasmine Unit test case for dynamically added component

I have a component in which i am dynamically adding another chart component. Here is the code:
appendBarChart(historicalData, key, type) {
// Create component dynamically inside the ng-template
const componentFactory =
this.componentFactoryResolver.resolveComponentFactory(this.injectableComponentClass);
const viewContainerRef = this.viewContainer;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
....
componentRef.instance.width = 300;
}
Here is the test case:
it('Should append historical data', inject([LineBarChartComponent], (childComponent: LineBarChartComponent) => {
component.appendBarChart(historicalData, 8, 'group');
fixture.detectChanges();
expect(childComponent.maxValue).toBe(5);
expect(childComponent.lineWithBar).toHaveBeenCalled();
}));
When I execute this test case, I get the following error:
Cannot read property "nativeElement" of undefined.
The child component has the following code:
constructor(private viewContainerRef: ViewContainerRef) {
this.elem = this.viewContainerRef.element.nativeElement;
}
Any ideas?

Elegantly inject pre-rendered element into Vue template

I'm trying to render an existing HTMLElement in a Vue component, like follows:
const someElement = document.createElement('p'); // for instance
Vue.component('my-component', {
data: () => ({ someElement }),
template: '<div>{{ someElement }}</div>',
// The above doesn't work; {{ moustache }} tags turn the element into a string.
});
There are several partial solutions, but none are perfect:
I could use v-html, which will work for some elements, but will fail to preserve e.g. event listeners.
Vue.component('my-component', {
data: () => ({ someElement }),
template: '<div v-html="someElement.outerHTML"></div>',
});
I could manually inject the element on the mounted() hook, which will work, but is somewhat ugly and feels unidiomatic.
Vue.component('my-component', {
data: () => ({ someElement }),
template: '<div></div>',
mounted() {
this.$el.appendChild(someElement);
},
});
If I could somehow wrap the element in a component, I could render that wrapper component. But it's unclear how to do this. I could use a manual render() function, but render() seems to have to return a VNode instance, and I see no way to turn an HTMLElement object into a VNode instance. A way shouldn't be expected to exist, given that VNode represents a virtual DOM whereas HTMLElement is part of the actual DOM.
Vue.component('element-wrapper', {
render() {
return someElement; // can't, not a VNode
},
});
Vue.component('my-component', {
template: '<div><element-wrapper></element-wrapper></div>',
});
I could use :is, but :is expects a component options object, meaning this option has all the same difficulties as #3.
Vue.component('my-component', {
template: '<div><component :is="elementAsComponent"></component></div>',
data: () => ({
elementAsComponent: /* again need to wrap element in component */,
}),
});
Is there an elegant way to do this?
This is possible by wrapping the custom element in a very thin Vue component which just replaces its root element with the custom element on mount.
Like so (fiddle):
const myCustomElement = document.createElement('p');
myCustomElement.innerText = 'click me';
myCustomElement.addEventListener('click', () => {
myCustomElement.style.color = 'red';
})
/* Takes an HTMLElement and returns a wrapping Vue component options object */
function liftToVue(element) {
return {
template: '<div></div>',
mounted() {
this.$el.replaceWith(element);
},
// If element is or may be a Promise, you can do:
// async mounted() { this.$el.replaceWith(await element); }
};
}
Vue.component('my-component', {
data: () => ({ myCustomElementAsComponent: liftToVue(myCustomElement) }),
template: '<div><component :is="myCustomElementAsComponent" /></div>',
})
There is a high chance, however, that this is an XY problem. See if you can use Vue components in the first place instead of generating HTMLElement instances at all.

Angular2 Observables best practice

let's say I have a component which has a child component. For example this simple calendar:
Here is its template:
<month-board [current-month]="currentMonth$"></month-board>
<button (click)="decrement()">Prev</button>
<button (click)="increment()">Next</button>
when the buttons are clicked the month-board which subscribed to currentMonth$ is subscribed is changing the displayed month.
currentMonth$ type is Observable<Moment>.
My question is: is it a good practice to pass Observables to child components in Angular2? is there any better way to do this?
Parent full code:
#Component({
selector: 'month-view',
templateUrl: 'app/components/month-view/month-view.html',
styleUrls: ['app/components/month-view/month-view.css'],
directives: [MonthBoard],
})
export class MonthView {
currentMonth: Moment = moment();
currentMonth$: Observable<Moment>;
currentMonthObserver: Observer<Moment>;
decrement: Function;
increment: Function;
constructor() {
this.currentMonth$ = Observable.create((observer: Observer<Moment>) => {
this.currentMonthObserver = observer;
});
const decrementCounter$: Observable<Function> = Observable.create((observer) => {
this.decrement = () => {
observer.next();
};
});
const incrementCounter$: Observable<Function> = Observable.create((observer) => {
this.increment = () => {
observer.next();
};
});
this.currentMonth$.subscribe();
Observable
.merge(
decrementCounter$.map(() => - 1),
incrementCounter$.map(() => + 1)
)
.startWith(0)
.scan((currentCount: number, value: number) => currentCount + value)
.subscribe((count: number) => {
this.currentMonthObserver.next(this.currentMonth.clone().add(count, 'M'));
});
}
}
Child full code:
#Component({
selector: 'month-board',
templateUrl: 'app/components/month-board/month-board.html',
styleUrls: ['app/components/month-board/month-board.css'],
directives: [DayCell]
})
export class MonthBoard implements OnInit{
#Input('current-month') currentMonth: Observable<Moment>;
weeks: Moment[][];
constructor(private calendarHelper: CalendarHelper) {
this.weeks = this.calendarHelper.getMonthWeeks();
}
ngOnInit() {
this.currentMonth.subscribe((month: Moment) => {
this.weeks = this.calendarHelper.getMonthWeeks(month);
});
}
}
You can do it that way, the other way is with #input. It's very easy to pass values from parent to child with it.
https://angular.io/docs/ts/latest/api/core/Input-var.html
I don't think it's necessarily bad to pass observables that way to your child component. For example I have a service that uses an observable that my whole application uses to watch for logged in events. But for a Calendar you might find yourself wanting to pass different values in different places on the same observable. If you do that you can always make another observable. Or manipulate it in different ways for each component.
But for readability I would definitely just use #input, that way I only have to go to the parent component to figure out what I am passing around.

Categories