Angular 6 - a guide to mocking router.events URL responses for unit testing

Looking to simulate router.events in a unit test, as suggested by the title.

Within my component, I am using regex to extract the first occurrence of text between slashes in the URL; for example, /pdp/

  constructor(
    private route: ActivatedRoute,
    private router: Router,
  }

this.router.events.pipe(takeUntil(this.ngUnsubscribe$))
      .subscribe(route => {
        if (route instanceof NavigationEnd) {
        // debugger
          this.projectType = route.url.match(/[a-z-]+/)[0];
        }
      });

Encountering errors in my unit tests during component setup: Cannot read property '0' of null. When I analyze with the debugger enabled, the value of route does not appear to be properly set, although I have defined it in the unit test itself. Various approaches have been attempted based on resources like this post: Mocking router.events.subscribe() Angular2 and others.

Initial approach:

providers: [
        {
          provide: Router,
          useValue: {
            events: of(new NavigationEnd(0, '/pdp/project-details/4/edit', 'pdp/project-details/4/edit'))
          }
        },
        // Mocking ActivatedRoute as well
        {
          provide: ActivatedRoute,
          useValue: {
            snapshot: { url: [{ path: 'new' }, { path: 'edit' }] },
            parent: {
              parent: {
                snapshot: {
                  url: [
                    { path: 'pdp' }
                  ]
                }
              }
            }
          }
        }
]

Second attempt (following the aforementioned post):

class MockRouter {
  public events = of(new NavigationEnd(0, '/pdp/project-details/4/edit', '/pdp/project-details/4/edit'))
}

providers: [
        {
          provide: Router,
          useClass: MockRouter
        }
]

Third attempt (also inspired by the above post):

class MockRouter {
  public ne = new NavigationEnd(0, '/pdp/project-details/4/edit', '/pdp/project-details/4/edit');
  public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });
}

providers: [
        {
          provide: Router,
          useClass: MockRouter
        }
]

Fourth attempt:

beforeEach(() => {
    spyOn((<any>component).router, 'events').and.returnValue(of(new NavigationEnd(0, '/pdp/project-details/4/edit', 'pdp/project-details/4/edit')))
...

Fifth attempt:

beforeEach(() => {
    spyOn(TestBed.get(Router), 'events').and.returnValue(of({ url:'/pdp/project-details/4/edit' }))
...

In all the instances mentioned above, the variable route remains unset; the NavigationEnd object is displayed as:

{ id: 1, url: "/", urlAfterRedirects: "/" }

Any insights or suggestions?

Answer №1

Here is an example that demonstrates how easy it can be:

By using the TestBed.configureTestingModule() method with imports like [RouterTestingModule], we can easily compile the components.
    
...
    
In this snippet of code, a new NavigationEnd event is created with parameters (42, '/', '/'). By injecting the Router through TestBed.inject(Router) and emitting this event using the .next() method on the events Subject<Event>, we can simulate a navigation event.

Answer №2

After analyzing the problem thoroughly, I have crafted a solution that involves diving deeper into the initial approach:

import { of } from 'rxjs';
import { NavigationEnd, Router } from '@angular/router';

providers: [
    {
      provide: Router,
      useValue: {
        url: '/non-pdp/phases/8',
        events: of(new NavigationEnd(0, 'http://localhost:4200/#/non-pdp/phases/8', 'http://localhost:4200/#/non-pdp/phases/8')),
        navigate: jasmine.createSpy('navigate')
      }
    }
]

Answer №3

Here is a helpful tip: include a footer component with routerLinks in the template to avoid the "root not provided" error. In order to control navigation events, use a "real" router with a spied variable directing router.events to a test observable.

export type Spied<T> = { [Method in keyof T]: jasmine.Spy };

describe("FooterComponent", () => {
   let component: FooterComponent;
   let fixture: ComponentFixture<FooterComponent>;
   let de: DebugElement;
   const routerEvent$ = new BehaviorSubject<RouterEvent>(null);
   let router: Spied<Router>;

 beforeEach(async(() => {
    TestBed.configureTestingModule({
        providers: [provideMock(SomeService), provideMock(SomeOtherService)],
        declarations: [FooterComponent],
        imports: [RouterTestingModule]
    }).compileComponents();

    router = TestBed.get(Router);
    (<any>router).events = routerEvent$.asObservable();

    fixture = TestBed.createComponent(FooterComponent);
    component = fixture.componentInstance;
    de = fixture.debugElement;
}));
// component subscribes to router nav event...checking url to hide/show a link
it("should return false for showSomeLink()", () => {
    routerEvent$.next(new NavigationEnd(1, "/someroute", "/"));
    component.ngOnInit();
    expect(component.showSomeLink()).toBeFalsy();
    fixture.detectChanges();
    const htmlComponent = de.query(By.css("#someLinkId"));
    expect(htmlComponent).toBeNull();
});

Answer №4

After exploring different options, I discovered a new method that is compatible with RouterTestingModule, similar to what @daniel-sc suggested.

   const events = new Subject<{}>();
   const router = TestBed.get(Router);
   spyOn(router.events, 'pipe').and.returnValue(events);
   events.next('Result of pipe');

Answer №5

Just wanted to share what I did to address this:

const eventsData = new BehaviorSubject<Event>(null);
    export class RouterData {
      eventsList = eventsData;
    }

    beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [AppComponent],
          imports: [RouterTestingModule],
          providers: [
            { provide: HttpClient, useValue: {} },
            { provide: Router, useClass: RouterData },
            Store,
            CybermetrieCentralizedService,
            TileMenuComponentService,
            HttpErrorService
          ],
          schemas: [NO_ERRORS_SCHEMA]
        }).compileComponents();
        fixture = TestBed.createComponent(AppComponent);
        routerData = TestBed.get(Router);
        tileMenuService = TestBed.get(TileMenuComponentService);
        component = fixture.componentInstance;
      }));

During the testing phase, I followed these steps:

     it('should trigger menu close on dashboard navigation completion', async(() => {
        spyOn(tileMenuService.menuOpened, 'next');
        component.checkRouterEvents();
        routerData.eventsList.next(new NavigationEnd (1, '/', '/'));
        expect(tileMenuService.menuOpened.next).toHaveBeenCalledWith(false);
      }));

This was crucial for verifying if the expected action is executed after navigating to route '/'.

Answer №6

Whenever I find myself needing to unit test a router event, I always end up revisiting my old code and thinking "there has to be a better way". While this page contains some valuable insights and code snippets, there are a few things to keep in mind:

  • RouterTestingModule is no longer recommended
  • It's important to test with properly constructed objects for better (compliance / linting)
  • Writing source code solely for testing purposes is not ideal

Step 1 - Create quality test objects

const routerEvent$: Subject<RouterEvent> = new Subject<RouterEvent>();
const navStartEvent: NavigationStart = new NavigationStart(1, '/fakeUrl');
const navEndEvent: new NavigationEnd(1, '/fakeUrl', '/anotherFakeUrl');

Make sure to import necessary modules from @angular/router / rxjs

Step 2 - Inject these objects into your unit tests

beforeEach(() => {
 TestBed.configureTestingModule({
   ...
    providers: [
      {
        provide: Router,
        useValue: { events: routerEvent$ }
      }
    ]
  });

There are alternate methods available, but this approach aligns well with Angular conventions. A less favorable (incorrect) alternative involves adding .pipe() between router.events and .subscribe() in your component.ts file. In your .spec.ts file, you can simply do

spyOn(router.events, 'pipe').and.returnValue(routerEvent$);
for the unit tests.

Step 3 - Simulate specific router events during unit testing

it('should perform different actions on navigation start / end', () => {
  expect(loadingSvc.spinnerOn).not.toHaveBeenCalled();
  expect(loadingSvc.spinnerOff).not.toHaveBeenCalled();
  routerEvent$.next(navStartEvent); // Triggers navigation start event
  fixture.detectChanges();
  expect(loadingSvc.spinnerOn).toHaveBeenCalledTimes(1);
  expect(loadingSvc.spinnerOff).not.toHaveBeenCalled();
  routerEvent$.next(navEndEvent); // Triggers navigation end event
  fixture.detectChanges();
  expect(loadingSvc.spinnerOn).toHaveBeenCalledTimes(1);
  expect(loadingSvc.spinnerOff).toHaveBeenCalledTimes(1);
});

The example uses a loading spinner, but there are better ways to handle such scenarios available!

Answer №7

  • utilize the ReplaySubject<RouterEvent> to simulate the behavior of router.events
  • make sure to export it as a service for independent testing, allowing other components to benefit from this service as well ;)
  • opt for using filter over instanceof

source code

import {Injectable} from '@angular/core';
import {NavigationEnd, Router, RouterEvent} from '@angular/router';
import {filter, map} from 'rxjs/operators';
import {Observable} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class RouteEventService {
  constructor(private router: Router) {
  }

  subscribeToRouterEventUrl(): Observable<string> {
    return this.router.events
      .pipe(
        filter(event => event instanceof NavigationEnd),
        map((event: RouterEvent) => event.url)
      );
  }
}

test code

import {TestBed} from '@angular/core/testing';

import {RouteEventService} from './route-event.service';
import {NavigationEnd, NavigationStart, Router, RouterEvent} from '@angular/router';
import {Observable, ReplaySubject} from 'rxjs';

describe('RouteEventService', () => {
  let service: RouteEventService;
  let routerEventRelaySubject: ReplaySubject<RouterEvent>;
  let routerMock;

  beforeEach(() => {
    routerEventRelaySubject = new ReplaySubject<RouterEvent>(1);
    routerMock = {
      events: routerEventRelaySubject.asObservable()
    };

    TestBed.configureTestingModule({
      providers: [
        {provide: Router, useValue: routerMock}
      ]
    });
    service = TestBed.inject(RouteEventService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  describe('subscribeToEventUrl should', () => {
    it('return route equals to mock url on firing NavigationEnd', () => {
      const result: Observable<string> = service.subscribeToRouterEventUrl();
      const url = '/mock';

      result.subscribe((route: string) => {
        expect(route).toEqual(url);
      });

      routerEventRelaySubject.next(new NavigationEnd(1, url, 'redirectUrl'));
    });

    it('return route equals to mock url on firing NavigationStart', () => {
      const result: Observable<string> = service.subscribeToRouterEventUrl();
      const url = '/mock';

      result.subscribe((route: string) => {
        expect(route).toBeNull();
      });

      routerEventRelaySubject.next(new NavigationStart(1, url, 'imperative', null));
    });
  });
});

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Linting error: Unable to access properties of an undefined object (isStrict)

While setting up ESLint in an Angular project, I encountered an error when running the linter: Oops! Something went wrong! :( ESLint: 8.56.0 TypeError: Cannot read properties of undefined (reading 'isStrict') Occurred while linting C:\User ...

Obtain references to templates in component classes

<div> <input #ipt type="text"/> </div> Can the template access variable be retrieved from the component class? For example, is it possible to retrieve it as shown below: class XComponent{ somefunction(){ //Is it possible t ...

Angular 2 testing - Struggling to generate a new component

I am currently dealing with a complex app that consists of numerous modules housing various components, services, and other Angular2 features. As part of my testing strategy, I am using the TestBed approach to create unit tests using Jasmine with Karma. ...

Toggle the selection of all checkboxes in TypeScript

Help needed with creating a single select/deselect all checkbox in Typescript. The current code successfully selects all when checked but fails to deselect all when unchecked. selectAllLocations() { var selectAll = < HTMLInputElement > document. ...

Using a plain JavaScript React component within a TypeScript React project: A step-by-step guide

A coworker of mine used Typescript for a frontend React project. With their departure, I have been tasked by management to take over the project and deliver results from Day 1. While they are open to me using Javascript in the project, I am unsure how to i ...

What is the significance of setting "ResolveJsonModule" to true in the TypeScript compiler options for a node.js project when importing a JSON file?

The title serves as the description. I am curious about the underlying reason behind it. Could you please provide some insight? Thank you. ...

The error property is not found in the type AxiosResponse<any, any> or { error: AxiosError<unknown, any>; }

As a beginner with typescript, I am encountering some issues with the following code snippet import axios, { AxiosResponse, AxiosError } from 'axios'; const get = async () => { const url = 'https://example.com'; const reques ...

Assistance required in configuring eslint for a monorepo with Yarn 3 and TypeScript

I've been struggling to configure ESlint in my monorepo while using Yarn 3.2.4 as the package manager. To see an example project, check out this GitHub repository. Here is the structure of the project: /monorepo ├── /configs │ ├── /esl ...

Secure TypeScript Omit Utility for Ensuring Type Safety

I need to create a custom implementation of Lodash's _.omit function using plain TypeScript. The goal is for the omit function to return an object with specific properties removed, which are specified as parameters after the initial object parameter. ...

Converting a string into an array of objects using Angular TypeScript

Can anyone help me with converting the following string into an array of objects? {"Car": "[" {"Carid":234,"CompanyCode":null}"," {"Carid":134,"CompanyCode":"maruti"}"," {"Carid":145,"CompanyCode":"sedan"}"," "]" } I attempted using JSON.parse, ...

Hiding the header on a specific route in Angular 6

Attempting to hide the header for only one specific route Imagine having three different routes: route1, route2, and route3. In this scenario, there is a component named app-header. The goal is to make sure that the app-header component is hidden when t ...

Leveraging angular2-material with systemjs for Angular2 development

After completing the TUTORIAL: TOUR OF HEROES on this link, I attempted to integrate angular2-material into my project. Unfortunately, I am having issues with the CSS not displaying correctly. Can anyone provide insight into what I may be missing or doing ...

Error encountered: Parsing error in Typescript eslint - The use of the keyword 'import' is restricted

My CDK application is written in typescript. Running npm run eslint locally shows no errors. However, when the same command is executed in a GitLab pipeline, I encounter the following error: 1:1 error Parsing error: The keyword 'import' is r ...

Tips for displaying content in a dropdown list on the navbar using Bootstrap 4

I have coded a navigation bar that includes a dropdown menu. When I click on a tab in the navbar, it should load the corresponding content. <div class="navbar navbar-light bg-faded"> <ul class="nav navbar-nav"> <a class= ...

Optimal practices for checking the status of your request

In my Node.js backend, I used to include a boolean value in my response to indicate successful operations: if(req.body.user.username == null || req.body.user.username == '' || req.body.user.password == null || req.body.user.password == '&ap ...

Forever waiting: Angular HTTP requests stuck in limbo

Switching from MongoDB to MySQL for my Angular/NodeJS project has brought about some challenges, particularly with handling HTTP Requests. I have tried GET and POST requests, but GET always remains pending and eventually fails, while POST does not successf ...

Limiting the image width of ngx-image-cropper to the popup container dimensions

I am currently working with a popup that contains an image cropper using ngx-image-cropper (https://www.npmjs.com/package/ngx-image-cropper) <div mat-dialog-container> <image-cropper [imageBase64]="imageFile" [mainta ...

Is there a solution for onBlur() event in Angular 7's ngx-intl-tel-input?

I encountered a problem in Angular 7 while working with the ngx-intl-tel-input package. I am trying to validate a phone number on blur event. <ngx-intl-tel-input [cssClass]="'form-control'" [preferredCountries]="preferredCountries" ...

Angular 8 Navigation: URL changes but fails to redirect to intended destination

I'm currently working on an Angular project that consists of two main components: home and submit. Here is a snippet from my app-routing.module.ts: import { NgModule } from '@angular/core'; import { Routes, RouterModule, ActivatedRoute } f ...

Navigating through the file structure with systemjs-builder

My project is structured as follows: /node_modules /client |---/app | |---/app.module.ts | |---/main.ts |---/systemjs.config.js |---/index.html /server |---/server.js /tools |---/builder.js /package.json I am using angular2-rc5 I have exposed c ...