I've created an offcanvas component for angular, but I can't get my unit tests to work.
Here's the failing unit test (offcanvas-host.component):
describe('BsOffcanvasHostComponent', () => {
let component: BsOffcanvasTestComponent;
let fixture: ComponentFixture<BsOffcanvasTestComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ CommonModule, OverlayModule ],
declarations: [
// Unit to test
BsOffcanvasHostComponent,
// Mock dependencies
BsOffcanvasMockComponent,
BsOffcanvasHeaderMockComponent,
BsOffcanvasBodyMockComponent,
BsOffcanvasContentMockDirective,
// Testbench
BsOffcanvasTestComponent,
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BsOffcanvasTestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
type OffcanvasPosition = 'top' | 'bottom' | 'start' | 'end';
@Component({
selector: 'bs-offcanvas-test',
template: `
<bs-offcanvas [(show)]="isOffcanvasVisible" [position]="position" [hasBackdrop]="true" (backdropClick)="isOffcanvasVisible = false">
<div *bsOffcanvasContent>
<bs-offcanvas-header>
<h5>Offcanvas</h5>
</bs-offcanvas-header>
<bs-offcanvas-body>
<span>Content</span>
</bs-offcanvas-body>
</div>
</bs-offcanvas>`
})
class BsOffcanvasTestComponent {
isOffcanvasVisible = false;
position: OffcanvasPosition = 'start';
}
@Directive({ selector: '[bsOffcanvasContent]' })
class BsOffcanvasContentMockDirective {
constructor(offcanvasHost: BsOffcanvasHostComponent, template: TemplateRef<any>) {
offcanvasHost.content = template;
}
}
@Component({
selector: 'bs-offcanvas-holder',
template: `
<div>
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
</div>`,
providers: [
{ provide: BsOffcanvasComponent, useExisting: BsOffcanvasMockComponent }
]
})
class BsOffcanvasMockComponent {
constructor(@Inject(OFFCANVAS_CONTENT) contentTemplate: TemplateRef<any>) {
this.contentTemplate = contentTemplate;
}
contentTemplate: TemplateRef<any>;
}
@Component({
selector: 'bs-offcanvas-header',
template: `
<div >
<ng-content></ng-content>
</div>`
})
class BsOffcanvasHeaderMockComponent {}
@Component({
selector: 'bs-offcanvas-body',
template: `
<div >
<ng-content></ng-content>
</div>`
})
class BsOffcanvasBodyMockComponent {}
which tests the following component:
ngAfterViewInit() {
const injector = Injector.create({
providers: [
{ provide: OFFCANVAS_CONTENT, useValue: this.content },
],
parent: this.rootInjector,
});
const portal = new ComponentPortal(BsOffcanvasComponent, null, injector);
const overlayRef = this.overlayService.create({
scrollStrategy: this.overlayService.scrollStrategies.block(),
positionStrategy: this.overlayService.position().global()
.top('0').left('0').bottom('0').right('0'),
hasBackdrop: false
});
this.component = overlayRef.attach<BsOffcanvasComponent>(portal); // <-- The test fails here
this.component.instance.backdropClick
.pipe(takeUntil(this.destroyed$))
.subscribe((ev) => {
this.backdropClick.emit(ev);
});
this.viewInited$.next(true);
}
The error message is
Error: NG0302: The pipe 'async' could not be found in the 'BsOffcanvasComponent' component!. Find more at https://angular.io/errors/NG0302
Here's a minimal reproduction of the issue
How can tell the angular TestingModule to use the mock component instead of the initial component type when calling
overlayRef.attach<BsOffcanvasComponent>(portal)
command in my unit test?
EDIT
Sadly I'm still not getting it to work. Usually for unit testing in angular there are mainly 3 cases:
Components referenced through the use of the tagname in the template of the UTT (unit-to-test)
This you solve by creating a MockComponent with the same tagname and inputs/outputs.
Ancestoral components injected in the UTT
This you solve by creating a MockComponent with a provide
r on the decorator
@Component({
selector: 'bs-offcanvas-holder',
template: ``,
providers: [
{ provide: BsOffcanvasComponent, useExisting: BsOffcanvasMockComponent }
]
})
class BsOffcanvasMockComponent {}
Component types called directly from the UTT
this.component = overlayRef.attach<BsOffcanvasComponent>(portal);
Here I should be able to use the BsOffcanvasMockComponent
instead, without having the unit-test dragging the other file in the TestBed. So how can I solve this? Off course I can mock the CDK Overlay
service, but this still leaves me with the above line of code in my UTT, where the BsOffcanvasComponent
is litterally being dragged into the Testbed.
CodePudding user response:
I think you have to mock overlayService
create method which will have to have a mock attach
that has instance
and backdropClick
.
Something like this (follow lines with !!):
describe('BsOffcanvasHostComponent', () => {
let component: BsOffcanvasTestComponent;
let fixture: ComponentFixture<BsOffcanvasTestComponent>;
let mockOverlayService: jasmine.SpyObj<OverlayService>;
beforeEach(async () => {
// mock overlay attach
mockOverlayAttach = {
attach: () => {
return {
instance: {
backdropClick: of(true),
}
};
}
};
mockOverlayService = jasmine.createSpyObj<OverlayService>('OverlayService', ['create']);
mockOverlayService.create.and.returnValue(mockOverlayAttach);
await TestBed.configureTestingModule({
imports: [ CommonModule, OverlayModule ],
declarations: [
// Unit to test
BsOffcanvasHostComponent,
// Mock dependencies
BsOffcanvasMockComponent,
BsOffcanvasHeaderMockComponent,
BsOffcanvasBodyMockComponent,
BsOffcanvasContentMockDirective,
// Testbench
BsOffcanvasTestComponent,
],
// provide mock for OverlayService
providers: [
{ provide: OverlayService, useValue: mockOverlayService },
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BsOffcanvasTestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
CodePudding user response:
I was able to solve the problem by providing a factory in my runtime module (OffcanvasModule) and in my TestingModule. This eliminates the import
of the BsOffcanvasComponent in the testingmodule.
providers: [{
provide: 'PORTAL_FACTORY',
useValue: (injector: Injector) => {
return new ComponentPortal(BsOffcanvasComponent, null, injector);
}
}]
offcanvas-host.component.spec.ts
providers: [
{
provide: 'PORTAL_FACTORY',
useValue: (injector: Injector) => {
return new ComponentPortal(BsOffcanvasComponent, null, injector);
}
}
]
This solves the following error
The pipe 'async' could not be found in the 'BsOffcanvasComponent' component