Angular - Leveraging Jest and NgMocks to Mock Wrapper Components

Within our project, we have implemented a feature where user roles can be assigned to various elements in the application. These roles determine whether certain elements should be disabled or not. However, due to additional conditions that may also disable some buttons, our component takes control and disables its child if needed. This design allows us to easily display tooltips when an element is disabled.

// app-role-element
<div matTooltip="{{ tooltipMessage | translate }}" [matTooltipDisabled]="!isDisabled">
  <ng-content> </ng-content>
</div>
// .ts
export class AppRoleElement implements AfterConentInit {
  @Input('roles') roles: string[];
  @ContentChild('roleElement') roleElement: MatButton;

  constructor(...){}    

  ngAfterContentInit(): void {
    ...
    this.setDisabled();
  }

  setDisabled(): void {
    if (this.roleElement) {
      if (this.isDisabled) {
        this.roleElement.disabled = true; // disable if no role
      } else {
        this.roleElement.disabled = this.disableCondition; // disable if other condition
      }
    }
  }
}

// usage
<app-role-component
  [role]="['required-role']"
  [disableCondition]= "isRunning || isUnstable"
  [id]="startButtonRoleElem"
>
  <button mat-raised-button id="startBtnId" (click)="start()">Start</button>
</app-role-component>

While this approach works as intended, it poses challenges when it comes to unit testing. In the code snippet above, selecting the Start button by ID may bypass the role-element and trigger the remote service even when it shouldn't. Clicking on the button directly does not pass through the role element.

test('to prevent click on a start btn when form is invalid', () => {
  spectator.component.runEnabled$ = of(false);
  spectator.detectComponentChanges();

  const checkExportFolderSpy = jest.spyOn(spectator.component, 'checkExportFolder');
  spectator.inject(PreferencesService).validateCurrentExportPath.andReturn(of(VALIDATION_RESULT_OK));
  spectator.detectChanges();

  spectator.click('#startBtnId');
  spectator.detectChanges();

  expect(checkExportFolderSpy).not.toHaveBeenCalled();
  expect(dispatchSpy).not.toHaveBeenCalled();
});

We are using JEST with Spectator and NgMocks for testing purposes, and I am looking for a way to effectively mock this component. Any suggestions on how to handle this scenario? Should the click event be passed to the child, or should the child be disabled in some way? Your insights and recommendations would be greatly appreciated.

Answer №1

Your situation is quite complex.

The complexity arises from the following reasons:

  • The button functionality is disabled using MatButton, making it unmockable
  • We are required to test AppRoleElement, which also cannot be mocked
  • triggerEventHandler does not recognize the disabled attribute and always triggers clicks

As a result, our testing approach needs to include the following steps:

  • Maintain the current structure of AppRoleElement and MatButton
  • Create a specialized environment for both disabled and enabled scenarios
  • Manually click the button using nativeElement

This code snippet exclusively utilizes ng-mocks with simplified role detection.

import {AfterContentInit, Component, ContentChild, Input, NgModule,} from '@angular/core';
import {MatButton, MatButtonModule} from '@angular/material/button';
import {MockBuilder, MockRender, ngMocks} from 'ng-mocks';

@Component({
  selector: 'app-role-component',
  template: `
    <div>
      <ng-content></ng-content>
    </div>
  `,
})
class AppRoleElement implements AfterContentInit {
  @Input() public disable: boolean | null = null;
  @ContentChild(MatButton) public roleElement?: MatButton;

  public ngAfterContentInit(): void {
    this.setDisabled();
  }

  public setDisabled(): void {
    if (this.roleElement) {
      this.roleElement.disabled = this.disable;
    }
  }
}

@NgModule({
  declarations: [AppRoleElement],
  imports: [MatButtonModule],
})
class AppModule {}

fdescribe('ng-mocks-click', () => {
  // Keeping AppRoleElement and MatButton
  beforeEach(() => MockBuilder(AppRoleElement, AppModule).keep(MatButton));

  it('is not able to click the disabled button', () => {
    // specific params for the render
    const params = {
      disabled: true,
      start: jasmine.createSpy('start'),
    };

    // rendering custom template
    MockRender(
      `
      <app-role-component
        [disable]="disabled"
      >
        <button mat-raised-button id="startBtnId" (click)="start()">Start</button>
      </app-role-component>
    `,
      params,
    );

    // clicking on a disabled element should have no effect
    ngMocks.find('button').nativeElement.click();

    // assertion
    expect(params.start).not.toHaveBeenCalled();
  });

  // checking the enabled case
  it('is able to click the button', () => {
    const params = {
      disabled: false,
      start: jasmine.createSpy('start'),
    };

    MockRender(
      `
      <app-role-component
        [disable]="disabled"
      >
        <button mat-raised-button id="startBtnId" (click)="start()">Start</button>
      </app-role-component>
    `,
      params,
    );

    ngMocks.find('button').nativeElement.click();
    expect(params.start).toHaveBeenCalled();
  });
});

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

When attempting to dispatch an action with switchMap, it fails, while map successfully triggers the

I recently started utilizing ngrx and ngrx/effects. In the code snippet below, I encountered an issue where using switchMap with concatLatestFrom and dispatching an action fails, while using map seems to work perfectly fine. Any idea why? When trying to ...

Struggling to Enforce Restricted Imports in TypeScript Project Even After Setting baseUrl and resolve Configuration

I am facing challenges enforcing restricted imports in my TypeScript project using ESLint. The configuration seems to be causing issues for me. I have configured the baseUrl in my tsconfig.json file as "src" and attempted to use modules in my ESLint setup ...

Issue with radio button validation not being triggered upon form submission

I am encountering an issue with validating a radio button in a form. Despite my efforts, I am able to proceed to the next page without selecting a radio button option. However, the validation for the email address field is working correctly. Below is the r ...

Tips for utilizing method overload in TypeScript with a basic object?

Looking at this code snippet: type Foo = { func(a: string): void; func(b: number, a: string): void; } const f: Foo = { func(b, a) { // ??? } } An error is encountered stating: Type '(b: number, a: string) => void' is not assign ...

Troubleshooting ngModel Binding Issue with Sub-Children in CKEditor 5 and Angular 7

Firstly, I am sharing my code below: ListPagesComponent: export class ListPagesComponent { public model = { id: -1, actif: 0, link: '', htmlContent: { fr: '', ...

Utilizing server-side caching middleware with tRPC version 10

Currently, I am working on a Next.js project and exploring the possibility of incorporating in-memory caching for tRPC results. Each tRPC procedure should have the option to set a custom TTL for caching purposes. My initial thought is that utilizing tRPC&a ...

Firestore TimeStamp.fromDate is not based on UTC timing

Does anyone have a solution for persisting UTC Timestamps in Firestore? In my Angular application, when I convert today's date to a Timestamp using the code below, it stores as UTC+2 (due to summer time in Switzerland). import {firebase} from ' ...

Challenges Faced with Implementing Active Reports in Angular 9

After following all the necessary steps outlined in this website to integrate Active Reports with Angular 9 (), I encountered an error when trying to compile my app: ERROR in The target entry-point "@grapecity/activereports-angular" has missing dependen ...

What is the best way to exceed the capacity of a function in TypeScript by incorporating optional

I've been working on converting some code from JavaScript to TypeScript, and I have a specific requirement for the return type of a function. The function should return void if a callback parameter is provided, otherwise it should return Promise<o ...

Labeling src library files with namespaces

I have developed a ReactJS website that interacts with a library called analyzejs which was created in another programming language. While I am able to call functions from this library, I do not have much flexibility to modify its contents. Up until now, ...

Measuring the duration of a unit test, from the setup phase to completion

Is there a way to accurately measure the time of an individual unit-test, considering the setup cost as well? I am facing a situation where my test base has a setup procedure that takes quite some time to complete. There are multiple tests derived from th ...

Using Angular 6 to Share Data Among Components through Services

I am facing an issue in my home component, which is a child of the Dashboard component. The object connectedUser injected in layoutService appears to be undefined in the home component (home userID & home connectedUser in home component logs); Is there ...

Angular project is showing a unique error that says '$localize' is undefined according to Eslint

I successfully implemented localization in my Angular 15 project. However, I encountered an issue with linting for .ts files: error '$localize' is not defined no-undef Here's the configuration in my .eslintrc.json file: { "root": true, ...

What is the best way to compare two sets of arrays and generate a new one with unique key-value pairs?

I have two arrays and I want to extract specific key-value pairs from each and combine them into a new array, handling duplicate entries. First Array : [ {id: 2, name: "HSBC", status: "YES"}, {id: 3, name: "Morgan Stanley&quo ...

Angular 7 flex layout ensures that the table fills its parent container completely without overstretching it

Hello, I'm encountering a problem with the sample found on StackBlitz. My goal is to confine the table to one page and have the remaining height filled after the header. If the table exceeds this height, I would prefer it to be hidden. However, based ...

WebStorm lacks support for TypeScript's `enum` and `readonly` features

When working with TypeScript in WebStorm, I encountered an issue where the IDE does not recognize enum or readonly. To solve this problem, I delved into TypeScript configuration files. I am currently utilizing .eslintignore, .eslintrc, tsconfig.json, and ...

The successful completion of an Angular2 HTTP request relies on the data obtained from a previous response

I developed a service that performs various http calls with different parameters. quote.service.ts getQuotes(){ let params = { "Type": "BasicDetail", } return this.http.post(this.url,params) .map(res => res.json()) } getOptio ...

Create Office Script calculations with quotations included

I am currently attempting to create an Excel office script formula for a cell. Are there any tips on how to insert a formula with quotes into a cell? For example, the following works fine: wsWa.getCell(WaRangeRowCount, 9).setFormula("= 1 + 1"); ...

What is the best way to set up TypeScript interfaces using predefined string literals to limit the possible data types for shared attributes?

In this scenario, we have two interfaces named A and B with validation and variant properties. The goal is to create an Example object by using only the variant and validation values provided (since field is already defined). However, I encountered an erro ...

The chai property "matchSnapshot" is not valid

https://i.sstatic.net/4wgqq.png After following the instructions provided at this link, I am now in the process of writing basic Jest tests for my React application that uses the Create-React-App starter kit. I came across a recommendation mentioned here ...