Angular Testing for Angular-Material on Mat-Menu
12,685
Solution 1
My final test ended up looking like this:
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { RouterTestingModule } from '@angular/router/testing';
import { AppRoutes } from './app.routes';
import {
MatToolbarModule,
MatIconModule,
MatMenuModule,
MatButtonModule
} from '@angular/material';
import { HomeComponent } from './home/home.component';
import { UserService } from './user/user.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { BehaviorSubject } from '../../node_modules/rxjs';
class MockUserService {
signedIn$: BehaviorSubject<boolean> = new BehaviorSubject(false);
signIn() {}
}
describe('AppComponent', () => {
let app: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let dom;
let button;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppComponent, HomeComponent],
providers: [{ provide: UserService, useClass: MockUserService }],
imports: [
NoopAnimationsModule,
MatIconModule,
MatToolbarModule,
MatMenuModule,
MatButtonModule,
RouterTestingModule.withRoutes(AppRoutes)
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
fixture.autoDetectChanges(true); //this was the key fix
spyOn(MockUserService.prototype, 'signIn').and.callThrough();
app = fixture.debugElement.componentInstance;
dom = fixture.nativeElement;
button = dom.querySelector('#userMenu');
});
it('should create the app', async(() => {
expect(app).toBeTruthy();
}));
describe('user menu', () => {
it('should not have the menu open', async () => {
const menu = dom.parentNode.querySelector('.mat-menu-panel');
expect(menu).toBeFalsy();
});
it('open the menu when clicking on the account button', async () => {
button.click();
const menu = dom.parentNode.querySelector('.mat-menu-panel');
expect(menu).toBeTruthy();
});
it('call user.signIn() once sign in button is pressed', async () => {
button.click();
const mockUser = TestBed.get(UserService);
dom.parentNode.querySelector('#userSignIn').click();
expect(mockUser['signIn']).toHaveBeenCalled();
});
});
describe('navigation icons', () => {
it('navigation icons show when user.signedIn$ emits true', async () => {
const mockUser = TestBed.get(UserService);
await mockUser.signedIn$.next(true);
await fixture.detectChanges();
expect(
dom.parentNode.querySelector('button[routerLink="/emails/received"]')
).toBeTruthy();
expect(
dom.parentNode.querySelector('button[routerLink="/emails/mapping"]')
).toBeTruthy();
});
it('navigation icons don\t show until user.signedIn$ emits true', async () => {
expect(
dom.parentNode.querySelector('button[routerLink="/emails/received"]')
).toBeFalsy();
expect(
dom.parentNode.querySelector('button[routerLink="/emails/mapping"]')
).toBeFalsy();
});
it('navigation icons don\t show until user.signedIn$ emits false', async () => {
const mockUser = TestBed.get(UserService);
await mockUser.signedIn$.next(false);
await fixture.detectChanges();
expect(
dom.parentNode.querySelector('button[routerLink="/emails/received"]')
).toBeFalsy();
expect(
dom.parentNode.querySelector('button[routerLink="/emails/mapping"]')
).toBeFalsy();
});
it('navigation icons re-hide on user.signedIn$ false', async () => {
const mockUser = TestBed.get(UserService);
await mockUser.signedIn$.next(true);
await fixture.detectChanges();
await mockUser.signedIn$.next(false);
await fixture.detectChanges();
expect(
dom.parentNode.querySelector('button[routerLink="/emails/received"]')
).toBeFalsy();
expect(
dom.parentNode.querySelector('button[routerLink="/emails/mapping"]')
).toBeFalsy();
});
});
});
Solution 2
There is simpler way:
you can use @ViewChild in component
@ViewChild('menuTrigger') menuTrigger: MatMenuTrigger;
in html template there is:
<button mat-icon-button [matMenuTriggerFor]="menu" #menuTrigger='matMenuTrigger'>
</button>
then there is "openMenu()" in test:
fit('should show menu', () => {
component.menuTrigger.openMenu()
}
Author by
TobyGWilliams
Updated on June 19, 2022Comments
-
TobyGWilliams about 2 years
I'm trying to write a test for my
mat-menu
in my application's toolbar. When I callbutton.click()
in my test, I get aCannot read property 'templateRef' of undefined
error in the console.As all works find in the browser, I believe this is to do with how I am running the test?
app.component.spec.ts
import { TestBed, async, ComponentFixture } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { RouterTestingModule } from '@angular/router/testing'; import { AppRoutes } from './app.routes'; import { MatToolbarModule, MatIconModule, MatMenuModule, MatButtonModule } from '@angular/material'; import { HomeComponent } from './home/home.component'; import { UserService } from './user/user.service'; class MockUserService { signIn() {} } describe('AppComponent', () => { let app: AppComponent; let fixture: ComponentFixture<AppComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [AppComponent, HomeComponent], providers: [{ provide: UserService, useClass: MockUserService }], imports: [ MatIconModule, MatToolbarModule, MatMenuModule, MatButtonModule, RouterTestingModule.withRoutes(AppRoutes) ] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AppComponent); app = fixture.debugElement.componentInstance; }); it('should create the app', async(() => { expect(app).toBeTruthy(); })); it('open the menu when clicking on the account button', async () => { const dom = fixture.debugElement.nativeElement; const button = dom.querySelector('#userMenu'); button.click(); //Error occurs on this line, before the console.log() console.log(button); fixture.detectChanges(); }); });
app.component.html
<mat-menu #menu="matMenu" [overlapTrigger]="false" yPosition="below"> //appears to not be able to find this element? <button mat-menu-item> <span>Settings</span> </button> <button id="userSignIn" (click)="user.signIn()" mat-menu-item> <span>Log In</span> </button> </mat-menu> <mat-toolbar color="primary"> <span>Report Receiver</span> <span class="fill-remaining-space"></span> <button *ngIf="user.signedIn$ | async" routerLink="/emails/mapping" mat-icon-button> <mat-icon>check_box</mat-icon> </button> <button *ngIf="user.signedIn$ | async" routerLink="/emails/received" mat-icon-button> <mat-icon>list</mat-icon> </button> <button routerLink="/home" mat-icon-button> <mat-icon>home</mat-icon> </button> <button id="userMenu" [matMenuTriggerFor]="menu" mat-icon-button> <mat-icon>account_circle</mat-icon> </button> </mat-toolbar> <router-outlet></router-outlet>
-
umutesen almost 5 yearsHow would this behave when there are more than one triggers on the component? e.g. a menu trigger per table row.
-
Ben Boyle over 4 yearstry
@ViewChildren
to monitor multiple triggers. It auto updates if you add/remove rows too! — angular.io/api/core/ViewChildren