The challenge of injecting services into Jest test cases using Angular 17 Functional Resolver

Overview

Greetings,

I am currently in the process of updating my Angular application from version 14 to version 17. One of the tasks involved in this update is replacing the deprecated Resolver classes with the new ResolverFn implementation.

The challenge I'm facing is that after migrating, I am encountering difficulties injecting services without triggering the

inject() must be called from an injection context
error whenever I run test code (More details about the error can be found here).

Previous Resolver Implementation
@Injectable()
export class TimespanResolver {
  private memberGranularity$: Observable<GranularityType>;

  constructor(
    private timespanService: TimespanService,
    private coreCache: CoreCacheService
  ) {
    this.memberGranularity$ = this.coreCache
      .MemberSettings()
      .pipe(
        map((settings) =>
          settings == null
            ? GranularityType.Weekly
            : settings.DefaultGranularity
        )
      );
  }

  /**
   * Updates the timespan when the route changes
   * @param route -- route snapshot
   */
  resolve({ data }: ActivatedRouteSnapshot): Promise<Timespan> {
    const granularity$ = data.initGranularity
      ? of(data.initGranularity)
      : this.memberGranularity$;

    return firstValueFrom(
      granularity$.pipe(
        map((granularity) =>
          TimespanService.GetTimespanFromGranularity(granularity)
        ),
        tap((timespan) =>
          this.timespanService.UpdateTimespan(
            timespan,
            data.timespanDisplayText
          )
        )
      )
    );
  }
}
New Resolver Implementation
export const timespanResolver: ResolveFn<any> = (
  route: ActivatedRouteSnapshot,
  __: RouterStateSnapshot
) => {
  const coreCache = inject(CoreCacheService);  // Triggers an injection error
  const timespanService = inject(TimespanService); // Triggers an injection error

  let memberGranularity$: Observable<GranularityType>;

  memberGranularity$ = coreCache
      .MemberSettings()
      .pipe(
        map((settings) =>
          settings == null
            ? GranularityType.Weekly
            : settings.DefaultGranularity
        )
      );

    const granularity$ = route.data.initGranularity
    ? of(route.data.initGranularity)
    : memberGranularity$;

    return firstValueFrom(
      granularity$.pipe(
        map((granularity) =>
          TimespanService.GetTimespanFromGranularity(granularity)
        ),
        tap((timespan) =>
          timespanService.UpdateTimespan(
            timespan,
            route.data.timespanDisplayText
          )
        )
      )
    );
};
Current Test Scenario
it('should update with default granularity from user settings if one isnt specified', fakeAsync(() => {
        // arrange
        jest.spyOn(service, 'UpdateTimespan');
        const newMoment = Date.UTC(2019, 1, 0);
        const realDateNow = Date.now.bind(global.Date);
        const dateNowStub = jest.fn(() => newMoment.valueOf());
        global.Date.now = dateNowStub;
        TimespanService._masterMomentRef = moment();
        jest.spyOn(TimespanService, 'GetTimespanFromGranularity');
        let snapshot: ActivatedRouteSnapshot = {
            data: {}
        } as ActivatedRouteSnapshot;

        let stateSnap: RouterStateSnapshot = {
        } as RouterStateSnapshot;

        // act
        timespanResolver(snapshot, stateSnap);
        flush();

        let res: Timespan;
        service.Timespan$.subscribe(t => res = t);
        flush();

        let startDate = res.StartDate.year() !== res.EndDate.year() ?
                        res.StartDate.format('MMMM Do, YYYY')
                        : res.StartDate.format('MMMM Do');
        let endDate = res.EndDate.month() !== res.StartDate.month() ?
                        res.EndDate.format('MMMM Do, YYYY')
                        : res.EndDate.format('Do, YYYY');

        // assert
        expect(service.UpdateTimespan).toHaveBeenCalledTimes(1);
        expect(TimespanService.GetTimespanFromGranularity).toHaveBeenCalledWith(GranularityType.Weekly);
        expect(res.Granularity).toBe(Weekly);
        expect(res.DisplayDate).toMatch(`${ startDate } - ${ endDate }`);
        global.Date.now = realDateNow;
        TimespanService._masterMomentRef = moment();
    }));
Approaches Tried So Far
  • Attempting to inject within the input parameters of the lambda expression (resulted in same error):
export const timespanResolver: ResolveFn<any> = (
  route: ActivatedRouteSnapshot,
  __: RouterStateSnapshot,
  coreCache = inject(CoreCacheService),
  timespanService = inject(TimespanService)
) => {
// Code removed for brevity
}
  • Introducing a class to handle the injected classes thinking constructors are considered within the injection context (resulted in same error);
export const timespanResolver: ResolveFn<any> = (
  route: ActivatedRouteSnapshot,
  __: RouterStateSnapshot
) => {
  let services = new ResolverServices();
  let coreCache = services.coreCache;
  let timespanService = services.timespanService;
  // Remaining code removed for brevity
};

class ResolverServices() {
  public coreCache = inject(CoreCacheService);
  public timespanService = inject(TimespanService);

  constructor() {
  }
}
  • Exploring ways to obtain an Injector from the resolver to utilize runInInjection but unable to find any resources or examples.

Answer №1

My app was running smoothly with the Resolver, but a new issue cropped up during Jest testing.

It turned out that when the resolver was called from the Jest test, it was outside of the injection context, leading to an error.

To resolve this issue, I made a simple adjustment by enclosing the resolver function call in TestBed.runInInjectionContext:

it('should update with default granularity from user settings if one isnt specified', fakeAsync(() => {
        // Other code removed for brevity
        let snapshot: ActivatedRouteSnapshot = {
            data: {}
        } as ActivatedRouteSnapshot;

        let stateSnap: RouterStateSnapshot = {
        } as RouterStateSnapshot;

        // act
        TestBed.runInInjectionContext(() => {
            timespanResolver(snapshot, stateSnap);
        });
        flush();
        // Other code removed for brevity
    }));

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

Exploring the features of NextJS version 13 with the benefits

Starting from the 13th step, SSR is utilized by default and in order to opt for client side rendering you must specify it at the top like so: 'use client' Currently, my setup involves TypeScript and styled-component integration. Take a look at ...

Creating a magical narrative within an NX workspace with the help of a user interface library

Within my NX workspace, I have multiple applications and libraries. As someone new to Storybook, I followed the instructions found here: in order to incorporate Storybook support into a simple component library. This library consists of two Angular module ...

Using TypeScript for Immutable.js Record.set Type Validation

Currently, I'm utilizing Immutable.js alongside TypeScript for the development of a Redux application. In essence, the structure of my State object is as follows: const defaultState = { booleanValue: true, numberValue: 0, } const StateRecord = ...

What is the best way to utilize a negative glob pattern?

After our build process completes, we end up with both e2015 and es5 bundles. For example, in the dist directory, you will find files like these: /common-es2015.7765e11579e6bbced8e8.js /common-es5.7765e11579e6bbced8e8.js /custom.js We are trying to set u ...

Angular 2's abstract component functionality

What are the benefits of utilizing abstract components in Angular 2? For example, consider the following code snippet: export abstract class TabComponent implements OnInit, OnDestroy {...} ...

Retrieving information from the database and mapping it to an array

Currently in my Angular application, I have hardcoded values in an array and bound them to a dropdown list using the in-built control, which is functioning correctly. The current setup looks like this: export class UserDetailsComponent implements OnInit { ...

Passing information between two components in Angular 2

How can data be transferred between two components in an Angular 2 application when: - the two components are on different routes, with the only common component being the root component? - I have successfully used shared services or Singletons, but I am ...

What is the reason behind the smaller bundle size of Angular2 CLI with "--prod" compared to "--prod --aot"?

Using the latest angular-cli (beta-18) for my project has brought an interesting observation to light. Despite being in its early stages, I am puzzled by the fact that my final bundle size is actually smaller without ahead-of-time (AoT) compilation. Upon ...

Tips for extracting elements from an HTML document using Angular

I seem to be facing a small issue - I am trying to develop a form using Angular. Below is my HTML: <form [formGroup]="requeteForm" (ngSubmit)="ajouter()" *ngIf=" tables!= null"> <div class="form-group&quo ...

How to Retrieve Superclass Fields in Angular 5 Component

I have a superclass that provides common functionality for components. export class AbstractComponent implements OnInit { public user: User; constructor(public http: HttpClient) { } ngOnInit(): void { this.http.get<User>(& ...

Can you point me in the direction of the Monaco editor autocomplete feature?

While developing PromQL language support for monaco-editor, I discovered that the languages definitions can be found in this repository: https://github.com/microsoft/monaco-languages However, I am struggling to locate where the autocompletion definitions ...

Retrieve the injectable value when importing SubModule into the App Module

Let me provide some background information... I have a feature module that requires a string value to be passed to its forRoot static method when imported in app.module.ts, like this: @NgModule({ declarations: [ /* ... */ ], imports: [ My ...

Steps for customizing the default properties of a material ui component

Is there a way to change the style properties listed on the main element? height: 0.01em; display: flex; max-height: 2em; align-items: center; white-space: nowrap; } <InputAdornment position="end" > {"hello& ...

Encountering an error when attempting to access an object property dynamically by using a passed down prop as a variable in Vue 2 & Vuex

I have been struggling for hours to find a solution to this problem, but so far I've had no luck. I've looked at these two questions, but they didn't provide the answers I needed: Dynamically access object property using variable Dynamical ...

Error exporting variables in NodeJS causing confusion

I'm currently in the process of transitioning a Meteor application from TypeScript to Javascript. While working on the server side, I've encountered some issues with the import/export code that functioned smoothly in TypeScript but now seems to b ...

The entry for "./standalone" in the "@firebase/database-compat" package does not have any documented conditions

Upon running npm run build for my sveltekit project, I encountered the following error generated by vite: 7:55:49 PM [vite-plugin-svelte] When trying to import svelte components from a package, an error occurred due to missing `package.json` files. Contact ...

Transform HTML elements within an *ngFor iteration based on a specific variable in Angular 4

In my current project using Angular 4, I am faced with the task of dynamically modifying HTML tags within an *ngFor loop based on a variable. Here is the code snippet that represents my approach: <mat-card-content *ngFor="let question of questionGrou ...

How do I implement an array of objects using an interface in React and Typescript?

I'm working with an array of objects where the data is stored in a JSON file. Here's a glimpse of the JSON file: [ { "_id": "62bd5fba34a8f1c90303055c", "index": 0, "email": "<a href="/cdn-cgi/l/emai ...

Is there a way to reposition the delete action to the final column within an ng2 smart table?

Is there a way to move the delete action to the last column in an 'ng2' smart table? I am looking to have the delete action appear only in the last column of my table in the ng2 smart table. Can anyone provide assistance with this matter? Below ...

Modifying a group of Components in Angular using a Service

Managing a set of Components involves changing their properties using a Service. The Components have a minimal model and are meant to remain compact. They are being rendered with *ngFor. The Service possesses a large Object and should possess the abilit ...