Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

teardown.destroyAfterEach breaks change detection when false #444

Open
NechiK opened this issue Apr 10, 2024 · 4 comments
Open

teardown.destroyAfterEach breaks change detection when false #444

NechiK opened this issue Apr 10, 2024 · 4 comments

Comments

@NechiK
Copy link

NechiK commented Apr 10, 2024

Hi,

This issue is related to this thread.

I'm writing a test for a form wizard using Angular Testing Library, and my approach involves splitting each test case using the it or describe methods. To avoid mocking the user session and other application parts, I render the <app-root></app-root> component in beforeAll. This allows me to lead my test to the wizard and test each step.

However, Angular clears/destroys the component/view/app after each it call. This means that if I move all preparations (login, navigation to the wizard, etc.) to the first it and start testing the wizard in the next calls, the whole progress will be lost, and the view will contain only the body tag.

Thanks to @timdeschryver, I could avoid this behavior and turn off view destroying after each it call using the next configuration:

configureTestBed: (testBed): void => {
  testBed.configureTestingModule(
    {
      teardown: { destroyAfterEach: false },
    },
  );
},

But it looks like this configuration breaks change detection because nothing happens if I enter any data into the form or want to navigate.

Here's what my test looks like:

import { AppComponent } from './app.component';
import { RenderResult, render, screen } from '@testing-library/angular';
import { APP_ROUTES } from './app.routes';
import { userEvent } from '@testing-library/user-event';

describe('AppComponent', () => {
  const user = userEvent.setup();
  let component: RenderResult<AppComponent>;

  beforeAll(async () => {
    component = await render('<app-root></app-root>', {
      imports: [
        AppComponent,
      ],
      routes: APP_ROUTES,
      // This configuration keeps the component instance and render results between tests
      // But breaks change detection
      configureTestBed: (testBed): void => {
        testBed.configureTestingModule(
          {
            teardown: { destroyAfterEach: false },
          },
        );
      },
    });

    component.detectChanges();
  });

  it('should render loginBtn', () => {
    const loginBtn: HTMLButtonElement = component.getByText('Login').closest('button')!;
    expect(loginBtn).toBeTruthy();
  });

  it('should disable login button by default', () => {
    const loginBtn: HTMLButtonElement = component.getByText('Login').closest('button')!;
    expect(loginBtn.disabled).toBeTruthy();
  });

  it('should enter username value', async () => {
    const usernameInput: HTMLInputElement = screen.getByLabelText(/Username/i);

    await user.type(usernameInput, 'John Doe');
    expect(usernameInput.value).toBe('John Doe');
  });

  it('should have username value entered before, enter password and enable button', async () => {
    const usernameInput: HTMLInputElement = screen.getByLabelText(/Username/i);
    const passwordInput: HTMLInputElement = screen.getByLabelText(/Password/i);

    // Have username value entered before
    expect(usernameInput.value).toBe('John Doe');

    await user.type(passwordInput, 'mysuperpassword');
    expect(passwordInput.value).toBe('mysuperpassword');

    const loginBtn: HTMLButtonElement = component.getByText('Login').closest('button')!;

    // This test is failing because the change detection is not working when teardown.destroyAfterEach is set to false
    expect(loginBtn.disabled).toBeFalsy();
  });
});

I've created a repo with minimal reproduction code (not from a real app) and left a few comments there.

The main goal I want to achieve is to have one big test for each functionality in my app but be able to split each such test by test cases using it and describe to have better readability and maintenance.

P.S. @timdeschryver also suggested using ATL_SKIP_AUTO_CLEANUP, but it doesn't work.

@timdeschryver
Copy link
Member

@NechiK thanks for the reproduction.
I can't figure out what is going on here to be honest.

I trimmed down the example to the simplest form, and even without ATL this test fails.
My guess is that this is something on Angular's side.

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

describe('AppComponent', () => {
  let fixture: ComponentFixture<FixtureComponent>;
  beforeAll(async () => {
    TestBed.configureTestingModule({
      teardown: { destroyAfterEach: false },
    });
    fixture = TestBed.createComponent(FixtureComponent);
    fixture.detectChanges();
  });

  it('should disable the button', async () => {
    const loginBtn: HTMLButtonElement = fixture.debugElement.query(
      By.css('button'),
    ).nativeElement;
    const usernameInput: HTMLInputElement = fixture.debugElement.query(
      By.css('input'),
    ).nativeElement;

    expect(loginBtn).toBeTruthy();
    expect(loginBtn.disabled).toBeTruthy();

    usernameInput.value = 'John Doe';
    usernameInput.dispatchEvent(new Event('input'));
    expect(usernameInput.value).toBe('John Doe');

    fixture.detectChanges();
    await fixture.whenStable();
    // This test is failing because the change detection is not working when teardown.destroyAfterEach is set to false
    expect(loginBtn.disabled).toBeFalsy();
  });
});

@Component({
  selector: 'app-root',
  standalone: true,
  template: `<label>
      Username:
      <input name="username" type="text" [(ngModel)]="username" />
    </label>
    Values:
    {{ username }}

    <button [disabled]="!username">Login</button>`,
  imports: [FormsModule],
})
class FixtureComponent {
  username = '';
}

@NechiK
Copy link
Author

NechiK commented Apr 14, 2024

@timdeschryver, I started thinking about it when I finished this reproduction example. When we discussed this issue in a thread related to your article, I thought there was an issue with routing because this is the first thing my app does—it checks the URL and redirects to the correct tenant based on it. But it looks like the issue was in detecting changes in general.

However, I checked the TestBed code in the Angular repo and found nothing that could affect the detectChanges.

If you don't mind, I'll create an issue in the Angular repo and attach your trimmed example. This will provide a clean testing example without any side libraries.

@timdeschryver
Copy link
Member

@NechiK Yea, feel free to do that.

@NechiK
Copy link
Author

NechiK commented May 23, 2024

@timdeschryver I have a VERY interesting update about this topic. A few days ago, I migrated one of our Skeleton projects to Nx. When I checked if our base tests didn't break, I decided to check my example above. And it worked.

Here's how test-setup.ts file looks like.

globalThis.ngJest = {
  testEnvironmentOptions: {
    teardown: {
      destroyAfterEach: false,
      rethrowErrors: true,
    },
    errorOnUnknownElements: true,
    errorOnUnknownProperties: true,
  },
};
import 'jest-preset-angular/setup-jest';

I'll make an investigation later. Maybe there was a problem in the Jest configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants