npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@blotch/angular-lynx-testing-library

v22.2.2

Published

Testing utilities for AngularLynx apps

Readme

@blotch/angular-lynx-testing-library

Testing utilities for AngularLynx apps. Renders Angular standalone components through the Lynx PAPI pipeline into a JSDOM tree, then exposes the result via @testing-library/dom queries.

Installation

ng generate @blotch/angular-lynx:add-testing

It installs this package (and Vitest and jsdom, if the project doesn't have them; ng new adds both) and makes sure ng test runs through AngularLynx's unit-test builder. To install it by hand: npm install --save-dev @blotch/angular-lynx-testing-library.

Setup

None. Tests run with ng test, whose builder (@blotch/angular-lynx-build:unit-test, which ng add sets up) loads this package's setup before the tests whenever the project has it. There is no Vitest config or setup file to write.

The setup:

  • Enables Angular JIT compilation (@angular/compiler), for components compiled at runtime
  • Installs LynxTestingEnv on the jsdom window
  • Switches to the Lynx main thread (where PAPI functions live)
  • Polyfills PAPI functions the testing environment doesn't implement (__GetParent, __QuerySelector, etc.)
  • Installs @testing-library/jest-dom matchers

ng test compiles the tests ahead of time, like the app, and checks templates strictly, as the Angular Language Service does: a component that uses Lynx elements imports LYNX_ELEMENTS from @blotch/angular-lynx. The examples below do.

Without Angular CLI

Under plain Vitest, load the setup yourself and define the compile-time flags the AngularLynx runtime reads (the rsbuild plugin defines them in the app, and ng test sets them too):

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  define: {
    __DEV__: JSON.stringify(true),
    __MAIN_THREAD__: JSON.stringify(true),
    __WEB__: JSON.stringify(false),
    __PROFILE__: JSON.stringify(false),
    __ENABLE_SSR__: JSON.stringify(false),
  },
  // Angular's JIT compiler reads the metadata of legacy decorators.
  oxc: { decorator: { legacy: true } },
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['@blotch/angular-lynx-testing-library/setup'],
  },
});

API

render(component, options?)

Bootstraps a standalone Angular component and returns a JSDOM container with @testing-library/dom queries bound to it.

import { render } from '@blotch/angular-lynx-testing-library';

const { container, getByText, queryByRole } = await render(MyComponent);

render is async — it awaits Angular's bootstrapApplication. The initial change detection cycle runs before the promise resolves, so the container is ready to query immediately.

Options:

| Option | Type | Description | | ----------- | -------------------------------------- | --------------------------------------------------------------------- | | providers | (Provider \| EnvironmentProviders)[] | Additional providers injected into the app (services, tokens, mocks). |

provideZonelessChangeDetection() and provideRenderer() are always included — you do not need to provide them.

Return value:

| Property | Type | Description | | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------- | | container | Element | The JSDOM <page> root element. All rendered children are descendants. | | unmount | () => void | Destroys the Angular app. | | ...queries | — | All @testing-library/dom queries bound to container (getByText, queryByRole, findByTestId, etc.). |

cleanup()

Destroys the current Angular app and resets the Lynx environment. Called automatically after each test — you only need this for manual control.

import { cleanup } from '@blotch/angular-lynx-testing-library';

afterEach(cleanup);

Set ATL_SKIP_AUTO_CLEANUP=true to disable auto-cleanup.

waitForUpdate()

Waits for Angular's zoneless change detection scheduler to flush pending reactive updates, then forces a synchronous CD cycle.

import { waitForUpdate } from '@blotch/angular-lynx-testing-library';

const count = signal(0);
count.set(1);
await waitForUpdate();
// DOM now reflects count = 1

Call this after any signal mutation that happens outside an event handler (e.g. direct .set() in test code).

fireEvent

Dispatches Lynx events on JSDOM elements. Use the named helpers for standard Lynx events:

import { fireEvent } from '@blotch/angular-lynx-testing-library';

fireEvent.tap(element);
fireEvent.input(element, { value: 'hello' });
fireEvent.scroll(element, { scrollTop: 100 });

Named helpers dispatch a DOM event with the key "bindEvent:<name>" (e.g. "bindEvent:tap"), which matches the listener registered by __AddEvent when Angular processes a (bindtap) template binding.

To dispatch a raw DOM event directly:

fireEvent(element, new Event('bindEvent:tap'));

Available named helpers:

tap · longtap · touchstart · touchmove · touchcancel · touchend · longpress · scroll · scrollend · focus · blur · input · confirm · layoutchange · transitionend · animationend

eventMap

The full Lynx event registry. Each entry has a defaultInit object (currently empty for all events, but present for forward-compatibility).

import { eventMap } from '@blotch/angular-lynx-testing-library';

console.log(Object.keys(eventMap));
// ['tap', 'longtap', 'touchstart', ...]

Re-exports from @testing-library/dom

screen · within · getQueriesForElement

Usage

Rendering a component

import { Component } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import { render } from '@blotch/angular-lynx-testing-library';

@Component({
  selector: 'greeting',
  imports: [LYNX_ELEMENTS],
  template: `<text>Hello, {{ name }}!</text>`,
})
class GreetingComponent {
  name = 'Lynx';
}

it('renders the greeting', async () => {
  const { container } = await render(GreetingComponent);
  expect(container.querySelector('text')!.textContent).toBe('Hello, Lynx!');
});

Injecting providers

import { Component, InjectionToken, inject } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import { render } from '@blotch/angular-lynx-testing-library';

const GREETING = new InjectionToken<string>('GREETING');

@Component({
  selector: 'test-greeting',
  imports: [LYNX_ELEMENTS],
  template: `<text>{{ greeting() }}</text>`,
})
class GreetingComponent {
  greeting = inject(GREETING);
}

it('uses the injected value', async () => {
  const { container } = await render(GreetingComponent, {
    providers: [{ provide: GREETING, useValue: 'Hola' }],
  });
  expect(container.querySelector('text')!.textContent).toBe('Hola');
});

Reactive signals

Define the signal outside the component so both the component and the test can access it:

import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';

const count = signal(0);

@Component({
  selector: 'test-counter',
  imports: [LYNX_ELEMENTS],
  template: `<text>{{ count() }}</text>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class CounterComponent {
  count = count;
}

it('updates when the signal changes', async () => {
  const { container } = await render(CounterComponent);
  expect(container.querySelector('text')!.textContent).toBe('0');

  count.set(42);
  await waitForUpdate();

  expect(container.querySelector('text')!.textContent).toBe('42');
});

Handling events

AngularLynx event bindings use Lynx prefixes: (bindtap), (catchtap), (bindinput), etc.

import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import {
  render,
  fireEvent,
  waitForUpdate,
} from '@blotch/angular-lynx-testing-library';

@Component({
  selector: 'test-button',
  imports: [LYNX_ELEMENTS],
  template: `
    <view (bindtap)="increment()">
      <text>{{ count() }}</text>
    </view>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ButtonComponent {
  count = signal(0);
  increment() {
    this.count.update((n) => n + 1);
  }
}

it('increments on tap', async () => {
  const { container } = await render(ButtonComponent);

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

  expect(container.querySelector('text')!.textContent).toBe('1');
});

Control flow — @if

it('shows content conditionally', async () => {
  const visible = signal(true);

  @Component({
    selector: 'test-if',
    imports: [LYNX_ELEMENTS],
    template: `@if (visible()) {
      <text>Hello</text>
    }`,
    changeDetection: ChangeDetectionStrategy.OnPush,
  })
  class IfComponent {
    visible = visible;
  }

  const { container } = await render(IfComponent);
  expect(container.querySelector('text')).not.toBeNull();

  visible.set(false);
  await waitForUpdate();
  expect(container.querySelector('text')).toBeNull();
});

Control flow — @for

it('renders a dynamic list', async () => {
  const items = signal(['A', 'B', 'C']);

  @Component({
    selector: 'test-for',
    imports: [LYNX_ELEMENTS],
    template: `@for (item of items(); track item) {
      <text>{{ item }}</text>
    }`,
    changeDetection: ChangeDetectionStrategy.OnPush,
  })
  class ListComponent {
    items = items;
  }

  const { container } = await render(ListComponent);
  expect(container.querySelectorAll('text').length).toBe(3);

  items.set(['A', 'B', 'C', 'D']);
  await waitForUpdate();
  expect(container.querySelectorAll('text').length).toBe(4);
});

Architecture notes

Single-thread model

AngularLynx runs entirely on the Lynx main thread (__MAIN_THREAD__ = true). This differs from React Lynx and Vue Lynx, which split rendering across two threads:

| Library | Thread model | | --------------- | ---------------------------------------------------------------- | | React Lynx | Dual: background thread runs React, main thread applies PAPI ops | | Vue Lynx | Dual: background thread runs Vue, main thread applies PAPI ops | | AngularLynx | Single: Angular renderer calls PAPI directly on main thread |

In tests, this means there is no thread switching — __MAIN_THREAD__ stays true throughout, and PAPI functions (__CreatePage, __CreateView, etc.) are always available.

The render pipeline

render(Component)
  └─ bootstrapApplication(Component, {
       provideZonelessChangeDetection(),
       provideRenderer(),
       ...providers
     })
       └─ LynxDocument.createRootElement()
            └─ __CreatePage()  →  <page> appended to document.body
       └─ Angular renders component tree
            └─ __CreateView / __CreateText / …  →  JSDOM children
       └─ LynxRendererFactory2.end()
            └─ __FlushElementTree()  (no-op in tests)
  └─ appRef.tick()  →  force synchronous CD
  └─ return { container: elementTree.root, ...queries }

Event dispatch

AngularLynx registers event handlers via __AddEvent(element, eventType, eventName, { type: 'worklet', value: callback }). The testing environment stores this as a DOM event listener under the key "${eventType}:${eventName}" (e.g. "bindEvent:tap").

fireEvent.tap(el) dispatches a "bindEvent:tap" DOM event, which triggers the listener. The listener calls runWorklet(callback, [event]) which invokes the Angular handler directly on the main thread.

Template binding → PAPI registration → DOM listener key:

| Template | Listener key | | -------------- | ------------------ | | (bindtap) | bindEvent:tap | | (catchtap) | catchEvent:tap | | (bindinput) | bindEvent:input | | (bindscroll) | bindEvent:scroll |

Comparison with React/Vue Lynx testing libraries

| Feature | React Lynx | Vue Lynx | AngularLynx | | ----------------------- | ----------------------- | ----------------- | ----------------------- | | render() return | Sync | Sync | Async | | Thread model | Dual (BG+MT) | Dual (BG+MT) | Main only | | Auto-cleanup env var | PTL_SKIP_AUTO_CLEANUP | — | ATL_SKIP_AUTO_CLEANUP | | renderHook() | ✓ | — | — | | Snapshot serializer | ✓ (RefProxy) | — | — | | Vitest plugin | ✓ | — | — (ng test sets up) | | Named fireEvent helpers | 30+ | 16 | 16 | | Thread-aware fireEvent | ✓ | ✓ | — (single thread) | | Async state helper | waitSchedule() | waitForUpdate() | waitForUpdate() |