I have this standalone component in angular 18:
import {
Component,
ElementRef,
OnInit,
ViewChild,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ProductService } from '@services/product.service';
import { Product } from '@models/product.model';
import { AppModule } from './app.module';
import { MatIconModule } from '@angular/material/icon';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { UpdateProductComponent } from './modals/update.product/update.product.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PaginatedProducts } from '@models/paginate.product.model';
@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
imports: [
MatProgressSpinnerModule,
CommonModule,
MatProgressSpinnerModule,
AppModule,
MatIconModule,
MatFormFieldModule,
MatDialogModule,
],
})
export class AppComponent implements OnInit {
isLoading = true;
errorMessage = '';
products: Product[] = [];
isDown = false;
startX = 0;
scrollLeft = 0;
queryPaginationProduct: any = {};
constructor(
private productService: ProductService,
public dialog: MatDialog,
public snackBar: MatSnackBar,
) {}
ngOnInit() {
this.queryPaginationProduct = {
limit: 10,
page: 1,
total: 10,
};
this.getProducts();
}
getProducts() {
this.isLoading = true;
this.productService
.getProducts(
this.queryPaginationProduct.page,
this.queryPaginationProduct.limit,
)
.subscribe({
next: (response: PaginatedProducts) => {
this.products = response.products;
this.isLoading = false;
this.queryPaginationProduct = {
limit: response.limit,
page: response.page,
total: response.total,
};
this.snackBar.open(response.message, 'Cerrar', {
duration: 5000,
horizontalPosition: 'right',
verticalPosition: 'bottom',
});
console.log('snackBar.open called with:', response.message);
},
error: (error) => {
this.errorMessage = 'Failed to load data';
this.isLoading = false;
console.error(error);
},
});
}
onPageChange(event: any): void {
this.queryPaginationProduct.page = event.pageIndex + 1;
this.queryPaginationProduct.limit = event.pageSize;
this.getProducts();
}
trackByFn(index: any, item: Product) {
return item.id;
}
onUpdate(item: Product) {
const dialogRef = this.dialog.open(UpdateProductComponent, {
width: '800px',
disableClose: true,
hasBackdrop: true,
data: item,
});
dialogRef.afterClosed().subscribe((result) => {
if (result.status === 'ok') {
this.getProducts();
}
this.snackBar.open(result.message, 'Cerrar', {
duration: 5000,
horizontalPosition: 'right',
verticalPosition: 'bottom',
});
});
}
}
And I was trying to create the unit test of the function that opens the matdialog with this approach, that mocks the mat dialog component of angular material:
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Subject, of } from 'rxjs';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { AppComponent } from './app.component';
import { UpdateProductComponent } from './modals/update.product/update.product.component';
import { Product } from '@models/product.model';
import { NoopAnimationsModule, provideAnimations } from '@angular/platform-browser/animations';
import { provideHttpClient, withFetch } from '@angular/common/http';
describe('AppComponent (fully stubbed MatDialog)', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let matDialogStub: {
openDialogs: MatDialogRef<any>[];
afterOpened: Subject<MatDialogRef<any>>;
afterAllClosed: Subject<void>;
open: jasmine.Spy;
closeAll: () => void;
};
let snackBar: jasmine.SpyObj<MatSnackBar>;
beforeEach(async () => {
const afterOpenedSubject = new Subject<MatDialogRef<any>>();
const afterAllClosedSubject = new Subject<void>();
matDialogStub = {
openDialogs: [],
afterOpened: afterOpenedSubject,
afterAllClosed: afterAllClosedSubject,
open: jasmine.createSpy('open').and.callFake((componentOrTemplateRef, config) => {
const mockDialogRef = {
afterClosed: () => of({ status: 'ok', message: 'Product updated successfully' }),
close: () => {
const index = matDialogStub.openDialogs.indexOf(mockDialogRef as any);
if (index > -1) {
matDialogStub.openDialogs.splice(index, 1);
}
if (matDialogStub.openDialogs.length === 0) {
afterAllClosedSubject.next();
}
},
} as MatDialogRef<UpdateProductComponent>;
matDialogStub.openDialogs.push(mockDialogRef);
afterOpenedSubject.next(mockDialogRef);
return mockDialogRef;
}),
closeAll: () => {
matDialogStub.openDialogs.length = 0;
afterAllClosedSubject.next();
},
};
const snackBarSpy = jasmine.createSpyObj('MatSnackBar', ['open']);
await TestBed.configureTestingModule({
imports: [AppComponent, UpdateProductComponent, NoopAnimationsModule],
providers: [
{ provide: MatDialog, useValue: matDialogStub },
{ provide: MatSnackBar, useValue: snackBarSpy },
provideAnimations(),
provideHttpClient(withFetch()),
],
})
.overrideComponent(AppComponent, {
remove: {
imports: [
],
},
})
.compileComponents();
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
snackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj<MatSnackBar>;
});
it('should open UpdateProductComponent and handle afterClosed = {status: "ok"}', () => {
spyOn(component, 'getProducts');
const mockProduct: Product = {
id: 123,
name: 'Test Product',
category: 'Test Category',
price: 100,
quantity: 5,
};
component.onUpdate(mockProduct);
expect(matDialogStub.open).toHaveBeenCalledWith(UpdateProductComponent, {
width: '800px',
disableClose: true,
hasBackdrop: true,
data: mockProduct,
});
expect(component.getProducts).toHaveBeenCalled();
expect(snackBar.open).toHaveBeenCalledWith(
'Product updated successfully',
'Cerrar',
{
duration: 5000,
horizontalPosition: 'right',
verticalPosition: 'bottom',
}
);
});
});
However, the test is not working because I got this error:
Expected spy MatSnackBar.open to have been called with: [ 'Something went wrong', 'Cerrar', Object({ duration: 5000, horizontalPosition: 'right', verticalPosition: 'bottom' }) ] but it was never called.
Basically none of the spies are working.
You should focus on just mocking what is necessary for the test case and not try to replicate the internal working of the MatDialog
, this will only add to the complexity of the test case and become difficult to read for new developers.
component.dialog = {
open: () => {},
} as any;
spyOn(component.dialog, 'open').and.returnValue({
afterClosed: () => {
return of({
status: 'ok',
message: 'Product updated successfully',
});
},
} as any);
import {
TestBed,
ComponentFixture,
fakeAsync,
flush,
} from '@angular/core/testing';
import { Subject, of } from 'rxjs';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HarnessLoader } from '@angular/cdk/testing';
import {
SnackBarHarnessExample,
UpdateProductComponent,
} from './snack-bar-harness-example';
import {
NoopAnimationsModule,
provideAnimations,
} from '@angular/platform-browser/animations';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { MatDialogHarness } from '@angular/material/dialog/testing';
import { MatDialogModule } from '@angular/material/dialog';
describe('AppComponent (fully stubbed MatDialog)', () => {
let component: SnackBarHarnessExample;
let fixture: ComponentFixture<SnackBarHarnessExample>;
let dialogLoader: HarnessLoader;
let matDialogStub: {
openDialogs: MatDialogRef<any>[];
afterOpened: Subject<MatDialogRef<any>>;
afterAllClosed: Subject<void>;
open: jasmine.Spy;
closeAll: () => void;
};
let snackBar: jasmine.SpyObj<MatSnackBar>;
beforeEach(async () => {
const snackBarSpy = jasmine.createSpyObj('MatSnackBar', ['open']);
await TestBed.configureTestingModule({
imports: [
SnackBarHarnessExample,
UpdateProductComponent,
NoopAnimationsModule,
MatDialogModule,
],
providers: [
{ provide: MatSnackBar, useValue: snackBarSpy },
provideAnimations(),
provideHttpClient(withFetch()),
],
})
.overrideComponent(SnackBarHarnessExample, {
remove: {
imports: [],
},
})
.compileComponents();
fixture = TestBed.createComponent(SnackBarHarnessExample);
component = fixture.componentInstance;
snackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj<MatSnackBar>;
dialogLoader = TestbedHarnessEnvironment.documentRootLoader(fixture);
});
it('should open UpdateProductComponent and handle afterClosed = {status: "ok"}', fakeAsync(() => {
spyOn(component, 'getProducts');
const mockProduct: any = {
id: 123,
name: 'Test Product',
category: 'Test Category',
price: 100,
quantity: 5,
};
component.dialog = {
open: () => {},
} as any;
spyOn(component.dialog, 'open').and.returnValue({
afterClosed: () => {
return of({
status: 'ok',
message: 'Product updated successfully',
});
},
} as any);
component.onUpdate(mockProduct);
flush();
expect(component.dialog.open).toHaveBeenCalledWith(UpdateProductComponent, {
width: '800px',
disableClose: true,
hasBackdrop: true,
data: mockProduct,
});
expect(component.getProducts).toHaveBeenCalled();
expect(snackBar.open).toHaveBeenCalledWith(
'Product updated successfully',
'Cerrar',
{
duration: 5000,
horizontalPosition: 'right',
verticalPosition: 'bottom',
}
);
}));
});