@blotch/angular-lynx-testing-library

Renders a component through the Lynx renderer in a unit test, then finds and taps its elements with Testing Library queries. import { render } from '@blotch/angular-lynx-testing-library';

Usage

src/app/counter/counter.ts
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';

@Component({
  selector: 'app-counter',
  imports: [LYNX_ELEMENTS],
  template: `
    <view (bindtap)="increment()">
      <text>Taps: {{ taps() }}</text>
    </view>
  `,
})
export class Counter {
  readonly taps = signal(0);

  increment(): void {
    this.taps.update((n) => n + 1);
  }
}
src/app/counter/counter.test.ts
import {
  fireEvent,
  render,
  waitForUpdate,
} from '@blotch/angular-lynx-testing-library';
import { describe, expect, it } from 'vitest';

import { Counter } from './counter';

describe('Counter', () => {
  it('counts taps', async () => {
    const { container, getByText } = await render(Counter);

    fireEvent.tap(container.querySelector('view')!);
    await waitForUpdate();

    expect(getByText('Taps: 1')).toBeTruthy();
  });
});

Add the library with ng generate @blotch/angular-lynx:add-testing, and run tests with ng test. The unit-test builder loads the library's setup, so there's no setup file to write.

API

render(component, options?)

function render<Q extends Queries = typeof queries>(
  component: Type<unknown>,
  options?: RenderOptions<Q>,
): Promise<RenderResult<Q>>;

Bootstraps component as a Lynx app, renders it into the test's DOM, and resolves once the first render is done.

  • It first destroys the app a previous render() left, and clears document.body.
  • The app gets provideZonelessChangeDetection(), provideRenderer() and your options.providers.
  • Elements render under a <page> element, the result's container.

cleanup()

function cleanup(): void;

Destroys the rendered app. It runs after every test, so you rarely call it. To turn that off, set the environment variable ATL_SKIP_AUTO_CLEANUP=true.

waitForUpdate()

function waitForUpdate(): Promise<void>;

Waits one task (a setTimeout of 0), so the change detection Angular scheduled can run, then runs change detection once more. Await it after fireEvent or after setting a signal, before you check the rendered output.

fireEvent(element, event)

fireEvent(element: Element, event: Event): boolean;
fireEvent.tap(element: Element, init?: Record<string, unknown>): boolean;
// … one method per event in the Event reference

Fires a Lynx event at element, which runs the matching template handler, such as (bindtap). The methods and their bindings are in the Event reference.

  • init: properties to set on the event. { detail: { value: 'hi' } } arrives as $event.detail.value.
  • init.eventType: the binding to reach. 'bindEvent' (the default) reaches (bind*) handlers. Use 'catchEvent' for (catch*) handlers.

fireEvent doesn't render. The handler runs before fireEvent returns, so a spy or a signal you read directly already shows the change. Angular renders the result in a change detection it schedules for after the current task. Await waitForUpdate() before checking the rendered output.

eventMap

const eventMap: Record<string, { defaultInit: {} }>;

The events that fireEvent has a method for, keyed by event name.

Types

RenderOptions

type RenderOptions<Q extends Queries = typeof queries> = {
  providers?: (Provider | EnvironmentProviders)[];
  queries?: Q;
};
PropertyDescription
providersExtra providers for the test app, such as provideRouter(routes)
queriesCustom queries to bind to container, next to the standard @testing-library/dom ones

RenderResult

PropertyTypeDescription
containerElementThe <page> element that holds everything rendered
componentRefComponentRef<unknown>The root component. Use componentRef.injector.get(Token) to reach a service
unmount() => voidDestroys the app, like cleanup()
rerender(component, options?) => Promise<RenderResult>Renders another component in its place
asFragment() => DocumentFragmentA copy of container, for snapshot tests
getByText, …Query functionsEvery @testing-library/dom query, bound to container, plus your custom queries

componentRef.setInput() works under ng test, which compiles components ahead of time. Under plain Vitest, which compiles them at runtime, it can't see signal inputs. Set a writable signal on componentRef.instance instead.

Re-exports

ExportFromDescription
screen@testing-library/domQueries bound to document.body
within@testing-library/domQueries bound to an element you pass
getQueriesForElement@testing-library/domThe same as within
ComponentRef@angular/core (type)The type of componentRef

The package also has a @blotch/angular-lynx-testing-library/setup entry, which prepares the Lynx test environment. ng test loads it for you.