Determining the specific types of a utility function used in Angular testing

The function I developed allows for an optional array of objects containing a name and value property. My goal is to have the value property automatically infer or pass the type of its value. I have successfully implemented this with a single object, but when there are multiple objects, it retains the type of the first object only. Below is the utility function:

import { Type } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'

export type InputSignal = Record<string, any>

export type AdditionalProvider<K, V> = { name: K; value: Type<V> }

export type SetupTestOptions<K extends string, V> = {
  setInput?: InputSignal
  additionalProviders?: AdditionalProvider<K, V>[]
}

export function setupTest<T extends object, K extends string, V>(
  Component: Type<T>,
  options: SetupTestOptions<K, V> = {}
) {
  const fixture: ComponentFixture<T> = TestBed.createComponent(Component)
  const component: T = fixture.componentInstance

  if (options.setInput) {
    Object.keys(options.setInput).forEach(key => {
      if (!options.setInput) return
      fixture.componentRef.setInput(key, options.setInput[key])
    })
  }

  const providers = <Record<K, V>>{}

  if (options.additionalProviders) {
    options.additionalProviders.forEach(({ name, value }) => {
      providers[name] = TestBed.inject(value) <-- here is where I would like the types to be inferred.
    })
  }

  fixture.detectChanges()

  return { fixture, component, ...providers }
}

Below is an example showcasing how the function is used:

it('should route to the dashboard/home route if projectId is null', async () => {
    const { fixture, component, location, ngZone, router } = setupTest(DashboardComponent, {
      additionalProviders: [
        { name: 'location', value: Location },
        { name: 'router', value: Router },
        { name: 'ngZone', value: NgZone }
      ]
    })

    ngZone.run(() => {
      router.initialNavigation()
      component.ngOnInit()
    })

    fixture.whenStable().then(() => {
      expect(location.path()).toBe('/dashboard/home')
    })
  })

I have experimented with different approaches, such as having type V extend various Angular utility types and explicitly defining the types in this specific scenario. The closest I have come is creating a union of the three different services (Location | Router | NgZone), but this requires explicit casting when using them. My desire is for TypeScript to automatically determine the correct type based on the value and assign that type to the destructured name in the example.

Answer №1

I concur with Naren's viewpoint, however, I also believe that the type inference significantly simplifies the process of writing and maintaining tests. Here is a proposed solution:

import { Type } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'

export type InputSignal = Record<string, any>

export type AdditionalProviders = { name: string, value: Type<any> }[];

type GetType<T> = T extends { value: Type<infer V> } ? V : never;

type ProvidersReturnType<TP extends AdditionalProviders> = {
    [TK in TP[number]['name']]: GetType<Extract<TP[number], { name: TK }>>;
}

export type SetupTestOptions<TProviders extends AdditionalProviders> = {
    setInput?: InputSignal;
    additionalProviders?: TProviders;
}

export function setupTest<T extends object, const TP extends AdditionalProviders>(
    Component: Type<T>,
    options: SetupTestOptions<TP> = {}
) {
    const fixture: ComponentFixture<T> = TestBed.createComponent(Component)
    const component: T = fixture.componentInstance

    if (options.setInput) {
        Object.keys(options.setInput).forEach(key => {
            if (!options.setInput) return
            fixture.componentRef.setInput(key, options.setInput[key])
        })
    }

    const providers: ProvidersReturnType<TP> = {} as any;

    if (options.additionalProviders) {
        options.additionalProviders.forEach(({ name, value }) => {
            (providers as any)[name] = TestBed.inject(value); // < --here is where I would like the types to be inferred.
        });
    }

    fixture.detectChanges()
    return { fixture, component, ...providers }
}

Typescript Playground

https://i.sstatic.net/fzuRqEe6.png

Answer №2

Why burden yourself with typing in test cases when you can simply add as any at the end of an object and start testing right away? Consider this - if an interface calls for 10 fields but you only require one to test a function, is it really necessary to create the remaining nine unused fields?

This scenario serves as a perfect example of how strict typing may not always be essential in the realm of testing.

  it('should navigate to the dashboard/home route if projectId is null', async () => {
    const { fixture, component, location, ngZone, router } = setupTest(DashboardComponent, {
      additionalProviders: [
        { name: 'location', value: Location },
        { name: 'router', value: Router },
        { name: 'ngZone', value: NgZone }
      ]
    } as any)

    ngZone.run(() => {
      router.initialNavigation()
      component.ngOnInit()
    })

    fixture.whenStable().then(() => {
      expect(location.path()).toBe('/dashboard/home')
    })
  })

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

How can we ensure consistency between the models of our Spring Boot and Angular applications? Any other suggestions?

When working on client-server applications using spring-boot and angular, I often come across resources that explain the process of exposing REST endpoints in spring boot and consuming them in angular with an http client. The usual approach involves commu ...

Combining objects in a JavaScript array based on a specific property

Here is an array I have: var array = [ { category: 'Input', field: 0, value: '17' }, { category: 'Input', field: 0, value: '5' }, { category: 'Input', field: 0, value: '8' }, ...

Creating validation rules for a form that includes a From Date and a To Date

Looking to implement validation for the from date and to date fields within a template-driven form. Is there a way to ensure that the "from date" is always greater than the "to date," and vice versa? Additionally, I would like to find a way to reuse this ...

What is the recommended way to retrieve the Nuxt `$config` within Vuex state? Can it only be accessed through store action methods?

After using the dotenv library for my .env file, I had to change the runtimeConfig in order to secure my project's secret key. In my most recent project, I utilized nuxt version "^2.14" in SPA mode, so I only utilized "publicRuntimeConfig" in my nuxt ...

What could be the reason for receiving an undefined user ID when trying to pass it through my URL?

Currently, I am in the process of constructing a profile page and aiming to display authenticated user data on it. The API call functions correctly with the user's ID, and manually entering the ID into the URL on the front end also works. However, wh ...

Create a single declaration in which you can assign values to multiple const variables

As a newcomer to react/JS, I have a question that may seem basic: I need multiple variables that are determined by a system variable. These variables should remain constant for each instance. Currently, my approach is functional but it feels incorrect to ...

Tips on navigating an array to conceal specific items

In my HTML form, there is a functionality where users can click on a plus sign to reveal a list of items, and clicking on a minus sign will hide those items. The code structure is as follows: <div repeat.for="categoryGrouping of categoryDepartm ...

In IE, Angular Material dialogs close by moving to the top left corner of the page

Whenever a user clicks on the submit button, a confirmation modal pops up on the screen. The modal provides an option for the user to close it by clicking on a button. Here's a snippet of how it's implemented: HTML: <ng-template #confirmMod ...

Guide on incorporating Bootstrap 4 beta.2 into an Angular 4 project

After attempting to install Bootstrap 4 version beta.2 along with its dependencies (jquery and popper.js), I encountered a strange problem. A SyntaxError message kept appearing in the console, specifically stating "SyntaxError: export declarations may only ...

Why am I having trouble iterating through my array object?

Trying to showcase the contents of object within an array. However, unable to showcase any results. Why is this happening? This is what I've attempted so far: Demo available here: https://stackblitz.com/edit/ionic-dsdjvp?file=pages%2Fhome%2Fhome.ts ...

When examining an element in an Angular application using Chrome Dev Tools, why do I not observe raw HTML code?

Just as the title suggests, my question is: Even if browsers don't understand Angular, why am I able to see Angular elements while inspecting the DOM despite using AOT (Ahead-Of-Time Compilation) which means there is no Angular compiler in the browse ...

Prevent further selections by deactivating checkbox upon button click in Ionic framework

I am in the process of developing a new feature for my Ionic app that involves creating profile groups. Users are required to select profiles from a checkbox list, then click a button to create the group. Once created, the selected profiles should either d ...

Modifications to one-to-many or many-to-one connections in typeorm may result in inadvertent null values being set or the updates not being applied at all

After searching through various posts with similar issues, none of the solutions seem to work for my specific problem. It's possible that I may be missing something, which is why I'm reaching out for assistance. Describing the Functionality With ...

A guide on parsing a stringified HTML and connecting it to the DOM along with its attributes using Angular

Looking for a solution: "<div style="text-align: center;"><b style="color: rgb(0, 0, 0); font-family: "Open Sans", Arial, sans-serif; text-align: justify;">Lorem ipsum dolor sit amet, consectetur adipiscing e ...

What is the best way to access the value from a subscription?

I am trying to retrieve the previous route and display its value in the HTML file, but I am encountering an issue where nothing is being printed. Even though the value is correctly assigned outside the subscription, it remains null. Below is the code snip ...

Creating an array by extracting form values in Angular

In my component, I have the following function: updateInfo(info: NgForm) { const changedValues = Object.keys(info.controls) .filter(key => info.controls[key].dirty === true) .map(key => { return { control: key, value: info.co ...

Exploring proactive search using RxJS Observable compared to Subject

Two different methods have been presented for tackling the same issue, which is to conduct a real-time search for specific characters entered by the user in a text box. The first solution is derived from the ngrx example, while the second solution is from ...

Is Angular matSort set up properly if headers are clickable but nothing happens when sorting?

I imported the MatSortModule into my component and the headers are clickable with the arrows showing as expected. However, the columns do not sort: <table mat-table [dataSource]="dataSource" *ngIf="showTable" matSort> <ng-container *ngFor="let ...

Tips for upgrading Angular ComponentFactoryResolver and ComponentFactory within a service

Currently, I am in search of an alternative for the deprecated ComponentFactoryResolver and ComponentFactory classes in Angular 13. Both the official documentation and another Stack Overflow question recommend utilizing ViewContainerRef. However, my applic ...

Angular: cut down lengthy text with an ellipsis and display more upon click

I am currently working with Angular 8, Angular Material, and Bootstrap utilities. Below is the code snippet I have been using: <div class="p-1 text-break" *ngSwitchDefault [innerHTML]="cell.value | longText"& ...