teardown.destroyAfterEach breaks change detection when false
Opened this issue · 4 comments
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.
@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 = '';
}
@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.
@NechiK Yea, feel free to do that.
@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.