I have a simple Angular 2 component for an Ionic 2 app. The component uses some Ionic's markup, such as:
<ion-card>
<h3>{{ rawcontent.name }}</h3>
<p *ngIf="rawcontent.description">{{ rawcontent.description }}</p>
</ion-card>
the component .ts is something like:
import { Component, Input } from '#angular/core';
import { NavController } from 'ionic-angular/';
import { Content } from '../../pages/content/content';
#Component({
selector: 'content-detail',
templateUrl: 'content-detail.html'
})
export class ContentDetailComponent {
#Input('data') rawcontent: any = {};
constructor(public nav: NavController) {
}
//other methods
}
I'm trying to write an unit test for it, but I got this error so far:
'ion-card' is not a known element:
1. If 'ion-card' is an Angular component, then verify that it is part of this module.
2. If 'ion-card' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '#NgModule.schemas' of this component
to suppress this message.
I don't know what to do now. In this case, ion-card is an Angular component, I guess. So, what to do next? I think I have to change my beforeEach, addining some config. Can anyone help?
beforeEach(() => TestBed.configureTestingModule({
declarations: [ ContentDetailComponent ],
providers: [
{ provide: NavController, useClass: NavMock }
]})
);
You need to import ionicModule. Add this in configureTestingModule
imports: [
IonicModule,
],
This is a good starting blog for testing Ionic 2 apps.
You need to configure the app with Ionic module root in beforeEach:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MyApp]
providers: [
],
imports: [
IonicModule.forRoot(MyApp)
]
}).compileComponents();
}));
Related
I'm working through Angular's upgrade guide to learn how to embed AngularJS components in an Angular app. I've created a bare-bones Angular app using the Angular CLI and added a simple AngularJS module as a dependency.
When I run ng serve, the application compiles with no errors. However, at runtime, I get this message in the console:
Error: Trying to get the AngularJS injector before it being set.
What is causing this error, and how can I avoid it? I haven't deviated from the steps detailed in the upgrade guide.
Here's how I'm upgrading my AngularJS component inside my Angular app:
// example.directive.ts
import { Directive, ElementRef, Injector } from '#angular/core';
import { UpgradeComponent } from '#angular/upgrade/static';
// this is the npm module that contains the AngularJS component
import { MyComponent } from '#my-company/module-test';
#Directive({
selector: 'my-upgraded-component'
})
export class ExampleDirective extends UpgradeComponent {
constructor(elementRef: ElementRef, injector: Injector) {
// the .injectionName property is the component's selector
// string; "my-component" in this case.
super(MyComponent.injectionName, elementRef, injector);
}
}
And here's my app.module.ts:
// app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UpgradeModule } from '#angular/upgrade/static';
import { ExampleDirective } from './example.directive';
import { myModuleName } from '#my-company/module-test';
#NgModule({
declarations: [AppComponent, ExampleDirective],
imports: [BrowserModule, AppRoutingModule, UpgradeModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(private upgrade: UpgradeModule) {}
ngDoBootstrap() {
this.upgrade.bootstrap(document.body, [myModuleName], {
strictDi: true
});
}
}
I'm using Angular 5.2.0.
I faced the same issue, and finally solved it. There are some steps to follow before bootstrap an hybrid Angular/angularjs application.
Install the UpgradeModule npm install #angular/upgrade
Wrap your "CompanyModule" (the module where all your company components are registered) into a new angularjs module (for instance: Ng1Shared). If you not have a module for your company components, it must be created. Than downgrade AppComponent as shown below.
const MyCompanyModule = angular
.module('MyCompanyModule', [])
.component('myComponent', MyComponent)
.name;
const Ng1Shared = angular
.module('Ng1Shared', [MyCompanyModule])
.directive('appRoot', downgradeComponent({ component: AppComponent }))
.name;
Configure AppModule with basic imports (BrowserModule, CommonModule, UpgradeModule). Provide the angularjs' Injector to Angular; declare an "entryComponent" and remove the default bootstrap for AppComponent.
#NgModule({
imports: [BrowserModule, CommonModule, UpgradeModule],
declarations: [AppComponent],
providers: [{provide: '$scope', useExisting: '$rootScope'}], // REQUIRED
entryComponents: [AppComponent], // ADD AN ENTRY COMPONENT
// bootstrap: [AppComponent] MUST BE REMOVED
})
Set angularjs globally with a function provided by UpgradeModule itself and manually bootstrap Angular with DoBootstrap method provided by #angular/core.
export class AppModule implements DoBootstrap {
constructor(private upgrade: UpgradeModule) { }
public ngDoBootstrap(app: any): void {
setAngularJSGlobal(angular);
this.upgrade.bootstrap(document.body, [Ng1Shared], { strictDi: false });
app.bootstrap(AppComponent);
}
}
Create a wrapper directive for every angularjs component and add it to AppModule's declaration array.
#Directive({
selector: 'my-component'
})
export class MyComponentWrapper extends UpgradeComponent {
#Input() title: string;
constructor(elementRef: ElementRef, injector: Injector) {
super('myComponent', elementRef, injector);
}
}
I wrote a simple example available on stackblitz.
For example purposes I added angularjs MyCompanyModule to another angularjs module, called Ng1Module. As you can see also property binding between angularjs and angular component works fine.
I hope it can be useful.
https://github.com/angular/angular/issues/23141#issuecomment-379493753
you cannot directly bootstrap an Angular component that contains
upgraded components before bootstrapping AngularJS. Instead, you can
downgrade AppComponent and let it be bootstrapped as part of the
AngularJS part of the app:
https://stackblitz.com/edit/angular-djb5bu?file=app%2Fapp.module.ts
try to add an entryComponents to your AppModule like this :
...
#NgModule({
declarations: [AppComponent, ExampleDirective],
imports: [BrowserModule, AppRoutingModule, UpgradeModule],
entryComponents: [
AppComponent // Don't forget this!!!
],
providers: [],
// bootstrap: [AppComponent] // Delete or comment this line
})
...
Hello and thank you for your time!
I am learning how to use Angular and I am interested in learning how to test its Components.
Currently I am struggling because I have done the Tour of Heroes tutorial of the Angular page and I am testing the code to understand it better.
The point is that I am testing hero-details component which code is:
import {Component, OnInit, Input} from '#angular/core';
import {ActivatedRoute} from '#angular/router';
import {MyHeroService} from '../hero-service/my-hero.service';
import {Location} from '#angular/common';
import {Hero} from '../Hero';
#Component({
selector: 'app-hero-details',
templateUrl: './hero-details.component.html',
styleUrls: ['./hero-details.component.css']
})
export class HeroDetailsComponent implements OnInit {
#Input() hero: Hero;
constructor(private route: ActivatedRoute,
private myHeroService: MyHeroService,
private location: Location) {
}
ngOnInit(): void {
this.getHero();
}
getHero(): void {
const id = +this.route.snapshot.paramMap.get('id');
this.myHeroService.getHero(id)
.subscribe(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
}
And my test tries to prove that getHero() is called after creating the hero-details component:
import {HeroDetailsComponent} from './hero-details.component';
import {ActivatedRoute} from '#angular/router';
import {MyHeroService} from '../hero-service/my-hero.service';
import {MessageService} from '../message.service';
import {Location} from '#angular/common';
import {provideLocationStrategy} from '#angular/router/src/router_module';
import {BrowserPlatformLocation} from '#angular/platform-browser/src/browser/location/browser_platform_location';
describe('heroDetails', () => {
it('should call getHero after being created', () => {
const heroDetailsComponent = new HeroDetailsComponent(new ActivatedRoute(),
new MyHeroService(new MessageService([])),
new Location(provideLocationStrategy(new BrowserPlatformLocation(['anyParameter']), '/')));
spyOn(heroDetailsComponent, 'getHero');
heroDetailsComponent.ngOnInit();
expect(heroDetailsComponent.getHero()).toHaveBeenCalled();
});
});
The difficulty I am facing is when I try to create a new Location which is a required parameter for the Hero-datail component's constructor.
The first Location's parameter is a PlatformStrategy, so then I used the provider to build it. Also, the provider needs a PlatformLocation() which looks like is abstract so then I chose the only implementation I could find which is BrowserPlatformLocation.
After all that process, the test execution says:
And the browser never ends loading the suite:
The strange thing here is that the IDE indeed finds those modules because I can navigate to them.
Also if I comment out that test, the suite works well:
Additionaly I have also read:
https://angular.io/api/common/Location
https://www.tektutorialshub.com/location-strategies-angular/
https://angular.io/tutorial/toh-pt5
How could I test it in a correct way? Where could I find more information about doing this type of tests properly? How could do this test to mock easily that Location parameter?
Thank you for reading this
Location is a built-in service, you do not need to instantiate it, just mock it:
const locationStub = {
back: jasmine.createSpy('back')
}
Then in your providers array:
providers: [ ..., {provide: Location, useValue: locationStub} ],
Then in your test just call the components goBack method, then use the Injector to get the instance of your service stub, like this:
const location = fixture.debugElement.injector.get(Location);
And then just test, that the back function has been called:
expect(location.back).toHaveBeenCalled();
This should solve the problem. This is, as far as I have seen, the best way to deal with the built-in services, you don't need to test them (Angular team did that already), just mock them and make sure they have been called correctly
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroDetailsComponent ],
providers: [ MyHeroService ],
imports: [ RouterTestingModule ],
providers: [{ provide: Location, useClass: SpyLocation }]
})
.compileComponents();
}));
it('should logout from application', async(() => {
const location: Location = TestBed.get(Location);
expect(location.href).toContain('blablabla url');
}));
Use SpyLocation from #angular/router/testing
If I were you, I would not bother creating tests from scratch : the CLI creates pre-made tests.
In those tests, there's a TestBed, that is used to set up a testing module for testing your component. If you used it, you would only have to import a router testing module.
This would give something like this :
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroDetailsComponent ],
providers: [ MyHeroService ],
imports: [ RouterTestingModule ]
})
.compileComponents();
}));
And just with that, your whole routing strategy is mocked.
I have this very basic project:
https://stackblitz.com/edit/angular-rktmgc-ktjk3n?file=index.html
This is the code on: /index.html
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<div class="mat-app-background basic-container">
<br />
API reference for Angular Material slide-toggle<br /><br />
<select-reset-example>loading</select-reset-example>
<div style="margin-top:30px;">
<div style="color:#f00;margin-bottom:20px;">
Below is what I need to get it work like above (but it doesn't):
</div>
<mat-slide-toggle>Slide me!</mat-slide-toggle>
</div>
</div>
This is the code on: /app/select-reset-example.html
<mat-slide-toggle>Slide me!</mat-slide-toggle>
When loading the component: mat-slide-toggle through: select-reset-example it works, but when loading it directly on the index.html it doesn't.
My question is, how to configure the following /main.ts file in order to render the mat-slide-toggle directly on the index.html?
In case the scope be a problem, maybe is it possible to create a custom component which inherits from that mat-slide-toggle or MatSlideToggleModule class?
If possible, could you fork the project on stackblitz.com and give me the link
with a proper configuration?
import './polyfills';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { BrowserModule } from '#angular/platform-browser';
import { BrowserAnimationsModule } from '#angular/platform-browser/animations';
import { NgModule } from '#angular/core';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { MatSlideToggleModule } from '#angular/material';
import { SelectResetExample } from './app/select-reset-example';
import { HttpModule } from '#angular/http';
import { CdkTableModule } from '#angular/cdk/table';
#NgModule({
exports: [
MatSlideToggleModule,
]
})
export class DemoMaterialModule { }
#NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
HttpModule,
DemoMaterialModule,
ReactiveFormsModule,
],
entryComponents: [SelectResetExample],
declarations: [SelectResetExample],
bootstrap: [SelectResetExample],
providers: []
})
export class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
This is the structure of the project:
Thanks!
Indeed there are several options which could be applied here.
Attempt 1
At first glance we can easily solve your problem by simply importing MatSlideToggle component and adding it to the bootstrap array:
import { MatSlideToggle } from '#angular/material';
#NgModule({
...
bootstrap: [SelectResetExample, MatSlideToggle ]
^^^^^^^^^^^^^^
cool, it was very easy!!!
})
export class AppModule { }
https://stackblitz.com/edit/angular-rktmgc-7h51hh?file=main.ts
Hmm, seems we've broken everything:).
Why?
Angular bootstraps SelectResetExample component. During this process it creates mat-slide-toggle that is part of select-reset-example.html template.
So now our html has two mat-slide-toggle tags.
And then angular bootstraps second component (MatSlideToggle) that will be applied to the first mat-slide-toggle. This way we can see that the first working slider lost text Slide me!.
Attempt 2
Let's change order of our bootstrapping components:
#NgModule({
...
bootstrap: [ MatSlideToggle, SelectResetExample ]
})
export class AppModule { }
https://stackblitz.com/edit/angular-rktmgc-mkm7ry?file=main.ts
The second slider works now but wait... We again lost the text.
The main reason of this is that angular can't process projectable nodes on bootstrapping component.
Attempt 3
Angular gives the opportunity to override bootstrapping process by writing code within ngDoBootstrap method of #NgModule. Let's try...
import { ApplicationRef, ComponentFactoryResolver, Injector, NgModuleRef } from '#angular/core';
#NgModule({
// we replaced bootstrap option with entryComponents
entryComponents: [SelectResetExample, MatSlideToggle],
})
export class AppModule {
constructor(
private resolver: ComponentFactoryResolver,
private ngModule: NgModuleRef<any>) {}
ngDoBootstrap(appRef: ApplicationRef) {
const factory = this.resolver.resolveComponentFactory(MatSlideToggle);
const target = document.querySelector('mat-slide-toggle');
const compRef = factory.create(
Injector.NULL,
[Array.from(target.childNodes)], // passing projectable nodes
target,
this.ngModule);
appRef.attachView(compRef.hostView);
appRef.bootstrap(SelectResetExample);
}
}
https://stackblitz.com/edit/angular-rktmgc-ncyebq?file=index.html
Here i am bootstrapping our components throught custom method ngDoBootstrap. And it works but...
What is this? Do I really need to know this?
I don't think so. There must be some other way out.
Attempt 4
in order not to complicate our live we should follow the design of angular framework. For that it would be better to have one root component. Let's create it:
app.component.ts
#Component({
selector: '.mat-app-background.basic-container',
templateUrl: './app.component.html',
})
export class AppComponent {
}
app.component.html
<br />
API reference for Angular Material slide-toggle<br /><br />
<select-reset-example>loading</select-reset-example>
<div style="margin-top:30px;">
<div style="color:#f00;margin-bottom:20px;">
Below is what I need to get it work like above (but it doesn't):
</div>
<mat-slide-toggle>Slide me!</mat-slide-toggle>
</div>
module
declarations: [SelectResetExample, AppComponent],
bootstrap: [AppComponent],
index.html
<div class="mat-app-background basic-container"></div>
i moved styles to external resouces
Stackblitz Example
In the AppComponent, I'm using the nav component in the HTML code. The UI looks fine. No errors when doing ng serve. and no errors in console when I look at the app.
But when I ran Karma for my project, there is an error:
Failed: Template parse errors:
'app-nav' is not a known element:
1. If 'app-nav' is an Angular component, then verify that it is part of this module.
2. If 'app-nav' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '#NgModule.schemas' of this component to suppress this message.
In my app.module.ts:
there is:
import { NavComponent } from './nav/nav.component';
It is also in the declarations part of NgModule
#NgModule({
declarations: [
AppComponent,
CafeComponent,
ModalComponent,
NavComponent,
NewsFeedComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule,
ModalModule.forRoot(),
ModalModule,
NgbModule.forRoot(),
BootstrapModalModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
I'm using the NavComponent in my AppComponent
app.component.ts
import { Component, ViewContainerRef } from '#angular/core';
import { Overlay } from 'angular2-modal';
import { Modal } from 'angular2-modal/plugins/bootstrap';
import { NavComponent } from './nav/nav.component';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Angela';
}
app.component.html
<app-nav></app-nav>
<div class="container-fluid">
</div>
I have seen a similar question, but the answer in that question says we should add NgModule in the nav component that has a export in that, but I'm getting compile error when I do that.
There is also: app.component.spec.ts
import {NavComponent} from './nav/nav.component';
import { TestBed, async } from '#angular/core/testing';
import { AppComponent } from './app.component';
Because in unit tests you want to test the component mostly isolated from other parts of your application, Angular won't add your module's dependencies like components, services, etc. by default. So you need to do that manually in your tests. Basically, you have two options here:
A) Declare the original NavComponent in the test
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
NavComponent
]
}).compileComponents();
}));
B) Mock the NavComponent
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
MockNavComponent
]
}).compileComponents();
}));
// it(...) test cases
});
#Component({
selector: 'app-nav',
template: ''
})
class MockNavComponent {
}
You'll find more information in the official documentation.
You can also use NO_ERRORS_SCHEMA
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
https://2018.ng-conf.org/mocking-dependencies-angular/
For me importing the component in the parent resolved the issue.
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
NavComponent
]
}).compileComponents();
}));
Add this in spec of the parent where this component is used.
One more reason is that there can be multiple .compileComponents() for beforeEach() in your test case
for e.g.
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TestComponent]
}).compileComponents();
}));
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientModule],
declarations: [Test1Component],
providers: [HttpErrorHandlerService]
}).compileComponents();
});
Step 1: Create stubs at beginning of spec file.
#Component({selector: 'app-nav', template: ''})
class NavComponent{}
Step 2: Add stubs in component's declarations.
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent,
NavComponent
],
}).compileComponents();
Another source for this error are the tests for the parent component, app-root, that include the tag of this component, app-nav.
Even though the message is about app-nav, the child component, the fix should be added in the tests of app-root, the parent component.
The fix can be a mock:
app.component.spec.ts:
#Component({selector: 'app-nav', template: ''})
class NavComponentStub {
}
What happens is that you create the root component, with it's tests, they pass. Latter on you add the child component in the root component tag and now you have to update the root component tests even if you just added a tag in the template.
Also the message doesn't say which test fails and from the message you might be led to believe that it's the child component's tests, when in fact they are the parent's tests.
If you create a stub and still get the same error it might be because of --watch mode on. Try to stop it and run again.
Just moved over to Angular 2 recently and i am just trying to get my head around pretty much all of it.
I need to build and that just uses stand-alone components, I want to be able to utilise my components as follows.
<body>
<component-one></component-one>
<component-two></component-two>
</body>
I have got as far as getting these components to render out on the page the problem is when one of these component selectors are not present on the current page i get the following console error...
core.umd.js:2838 EXCEPTION: Error in :0:0 caused by: The selector "component-one" did not match any elements
Is there a way to only bootstrap only the relevant components?
Also, the "Angular 2 is running in the development mode. Call enableProdMode() to enable the production mode." console message comes in multiples times depending on how many components i have on the page, which makes me feel like i am missing something.
Module config
// Modules
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
// Components
import { ComponentOne } from './components/componentOne';
import { ComponentTwo } from './components/componentTwo';
#NgModule({
imports: [ BrowserModule ],
declarations: [ ComponentOne, ComponentTwo ],
bootstrap: [ ComponentOne, ComponentTwo],
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class AppModule {
constructor() {
}
}
You can omit the bootstrap option and implementing ngDoBootstrap() yourself.
And to conditionally bootstrap components, just do a querySelector before calling appRef.bootstrap(SomeComponent); to check whether the component is already on the page.
#NgModule({
imports: [ BrowserModule ],
declarations: [ ComponentOne, ComponentTwo ],
entryComponents: [ ComponentOne, ComponentTwo ]
})
export class AppModule {
ngDoBootstrap(appRef: ApplicationRef) {
if(document.querySelector('component-one')) {
appRef.bootstrap(ComponentOne);
}
if(document.querySelector('component-two')) {
appRef.bootstrap(ComponentTwo);
}
}
}
Note: entryComponents option is required
Finally in your index.html you can omit second tag and angular won't raise error:
<body>
<component-one></component-one>
</body>
Plunker Example
If you don't want to see message Angular 2 is running in the development mode. Call enableProdMode() to enable the production mode. you can just enable prod mode or use the following (Since 2.3.0) which is similar as above (i recommend to use the first solution):
#NgModule({
imports: [ BrowserModule ],
declarations: [ ComponentOne, ComponentTwo ],
entryComponents: [ComponentOne, ComponentTwo]
})
export class AppModule {
constructor(private resolver: ComponentFactoryResolver, private inj: Injector) {}
ngDoBootstrap(appRef: ApplicationRef) {
if(document.querySelector('component-one')) {
const compFactory = this.resolver.resolveComponentFactory(ComponentOne);
let compOneRef = compFactory.create(this.inj, [], 'component-one');
appRef.attachView(compOneRef.hostView);
compOneRef.onDestroy(() => {
appRef.detachView(compOneRef.hostView);
});
}
if(document.querySelector('component-two')) {
const compFactory = this.resolver.resolveComponentFactory(ComponentTwo);
let compTwoRef = compFactory.create(this.inj, [], 'component-one');
appRef.attachView(compTwoRef.hostView);
compTwoRef.onDestroy(() => {
appRef.detachView(compTwoRef.hostView);
});
}
appRef.tick();
}
}
It's just the same that angular does internally when bootstraping component
Plunker Example
See also
https://github.com/angular/angular/issues/11730
Angular2 - Component into dynamicaly created element