Skip to content

RFC: Modernize Angular Testing Library with a zoneless subpackage #562

Closed

Description

@timdeschryver

First of all, happy new year and my best wishes to you! 🕺💃

TLDR: I want to modernize Angular Testing Library (ATL)

History

ATL is created to test angular components with the following requisites:

  • for developers: make testing easier and maintainable
  • for users: provide a good experience with accessibility in mind

The more your tests resemble the way your software is used, the more confidence they can give you.

To achieve this ATL is as a wrapper around DOM Testing library, which allows you to interact with components and verify its behavior.

The main concepts for testing Angular components are the TestBed and the Angular change detection, these two are handled automatically by ATL. This means you (almost) don't have to fire change cycle manually (detectChanges()) because it's fired by the event.xyz(), findBy, and waitFor utility functions.

From the start the core API is almost unchanged, with some additions along the way to provide the functionality for new Angular API's.

The Problem

Angular has changed over the past year by providing us signals and a zoneless environment.
This also impacts the way we test components. Because it makes all(?) of the change detection cycles obsolete. I also don't want the test environment to trigger them, instead, I want a experience close to a "real production" experience.

The proposed solution

We can make this behavior configurable, but I want to take this opportunity to also remove deprecated features and less used features. Because many application rely on "older" Angular, I want to create a new subpackage where the automatic change detection will be removed and where many options and functionality will also be removed. This way the test suites for older applications or components are left untouched. In the future the new subpackage can become the default.

This means:

  • No invocations of detectChanges(), this is handled by Angular
  • Less custom code/behavior, use new Angular testing utilities
  • Further TestBed customizations can be handled within the configureTestBed callback

In code this translates to the following minimal API with only the core functions:

export interface RenderResult<ComponentType> {
  container: HTMLElement;
  debug: (
    element?: Element | Document | (Element | Document)[],
    maxLength?: number,
    options?: PrettyDOMOptions,
  ) => void;
  fixture: ComponentFixture<WrapperType>;
}

export interface RenderOptions<Q extends Queries = typeof queries> {
  configureTestBed?: (testbed: TestBed) => void;
  bindings?: Binding[];
  providers?: (Provider | EnvironmentProviders)[];
  queries?: Q;
}

Impact on you

As I mentioned earlier, this will not impact you in the short future.
But, if you want to test your new components that are zoneless you can make use of the new subpackage.

The end result should be a thinner layer around the Angular API.

For the new subpackage the requisites (mentioned at the top) are still answered.

// 👇 Import from subpackage
import {render, screen} from '@testing-library/angular/zoneless';

test('interacts with the component', async () => {
  // 👇 We can just re-use the render method
  await render(FixtureComponent);

  // 👇 Inputs and outputs can be set using the bindings API
  await render(FixtureComponent, {
      bindings: [...]
  });

  const incrementControl = screen.getByRole('button', { name: '+' });
  const decrementControl = screen.getByRole('button', { name: '-' });
  const valueControl = screen.getByTestId('value');

  // 👇 Use findBy instead of getBy so the component/signal can the value
  expect(await screen.findByText('5')).toBeInTheDocument();

  incrementControl.click();
  incrementControl.click();

  // 👇 we can just use vi.waitFor instead of ATL's waitFor
  await vi.waitFor(() => expect(valueControl).toHaveTextContent('7'));
})

The functionality that I want to remove:

  • Remove declarations, imports, componentProperties, componentInputs, inputs, componentOutputs, on in favor of the bindings API
  • Remove router functionality, this can be implemented on the user-side using configureTestBed
  • Remove deferBlock functionality, this can be implemented on the user-side using configureTestBed

With these changes we can still provide a good testing experience. By using the latest Angular testing utilities we remove some clutter.

Call to action

If you want to get involved feel free to add a comment (or send me a DM) with the following questions in mind:

  • Is there a better way to migrate existing test suites from "old" Angular to Modern Angular?
  • I want to hear from you what features should be migrated over to this new package.

Activity

  1. the-ult commented on Jan 5, 2026

    @the-ult
    Contributor

    Is there a way to leverage the Vitest Browser API more? page instead of render .. etc? Or would that defy the purpose of ATL too much?
    And would that cause to many breaking changes/non-compatibility when people are not using Vitest?

  2. timdeschryver commented on Jan 5, 2026

    @timdeschryver
    MemberAuthor

    Is there a way to leverage the Vitest Browser API more? page instead of render .. etc? Or would that defy the purpose of ATL too much? And would that cause to many breaking changes/non-compatibility when people are not using Vitest?

    We want to stay framework agnostic, so not using any testing library specific. This will prevent breaking changes when you want to use Vitest.
    For the naming, it will still be render to stay close to the Testing Library family.

  3. shaman-apprentice commented on Jan 15, 2026

    @shaman-apprentice

    Love the idea and thanks for reaching out and all your work!

    I am not sure about the docu updates. On first thought, as a consumer, I would like the example section to show the new thin package @testing-library/angular/zoneless and add references to more examples with the "old package".

    Two nitpicks:

    // 👇 Use findBy instead of getBy so the component/signal can the value

    I am not sure what the findBy can do instead of getBy?

    // 👇 we can just use vi.waitFor instead of ATL's waitFor
    await vi.waitFor(() => expect(valueControl).toHaveTextContent('7'));

    OPINIONATED: I like explicit imports instead of the global implicit onces and guess more people would use them, if examples would use them as well 😇

  4. timdeschryver commented on Jan 31, 2026

    @timdeschryver
    MemberAuthor

    @shaman-apprentice I missed your message, sorry.

    I am not sure about the docu updates. On first thought, as a consumer, I would like the example section to show the new thin package @testing-library/angular/zoneless and add references to more examples with the "old package".

    I think this will be the way when we're happy with the zoneless package.

    I am not sure what the findBy can do instead of getBy?

    findBy tries to find the element a few times, this will be required more because it can take a tick before the signal's value is set/rendered.

    I like explicit imports instead of the global implicit onces and guess more people would use them, if examples would use them as well 😇

    I feel the same way. This is just an example though, using the built-in waitFor of ATL will also work.

  5. shaman-apprentice commented on Feb 1, 2026

    @shaman-apprentice

    findBy tries to find the element a few times, this will be required more because it can take a tick before the signal's value is set/rendered.

    That makes sense, thanks for the clarification! I 100% agree on the content; maybe reformulate
    // 👇 Use findBy instead of getBy so the component/signal can the value -> // 👇 Use findBy instead of getBy so the component/signal has time to update the value

  6. christophw-bird commented on Mar 3, 2026

    @christophw-bird

    Why would we waitFor after click at all, can't we force changeDetection after every interaction or await a macrotask after every click under the hood?

    My preferred API would be:

    incrementControl.click();
    incrementControl.click();
    
    expect(valueControl).toHaveTextContent('7');
  7. timdeschryver commented on Mar 8, 2026

    @timdeschryver
    MemberAuthor

    Why would we waitFor after click at all, can't we force changeDetection after every interaction or await a macrotask after every click under the hood?

    My preferred API would be:

    incrementControl.click();
    incrementControl.click();

    expect(valueControl).toHaveTextContent('7');

    Good news, that will work (see #570).
    We should also await the result when a signal is updated directly.

  8. github-actions commented on Mar 17, 2026

    @github-actions

    🎉 This issue has been resolved in version 19.2.0 🎉

    The release is available on:

    Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions