Testing

Test your components with ng test. @blotch/angular-lynx-testing-library renders a component through AngularLynx's real renderer into jsdom, a browser-like environment that runs in Node, and gives you Testing Library queries to check the result.

Set up

ng generate @blotch/angular-lynx:add-testing
  • What it does. It installs the testing library, plus Vitest and jsdom if the project doesn't have them. It also makes sure ng test runs through the AngularLynx test builder.
  • No config files. ng test loads the library's setup by itself whenever the project has the library. You don't write a Vitest config or a setup file.
  • New components come with a test. From then on, ng generate component adds a *.test.ts that renders the component with render().

To install the library by hand instead:

npm install --save-dev @blotch/angular-lynx-testing-library

Run the tests

ng test                 # watches for changes in a terminal, runs once in CI
ng test --watch=false   # runs once
  • The builder. ng add points the test target in angular.json at @blotch/angular-lynx-build:unit-test. It's Angular's own unit-test builder (Vitest), set up for Lynx, so it takes the same options: include, coverage, setupFiles, providersFile and the rest.
  • Test files. It runs every *.spec.ts and *.test.ts file. Angular's TestBed works too, so the specs that ng generate service or ng generate pipe writes run as they are.
  • Strict templates. Tests compile ahead of time, like the app, and their templates are checked strictly. A component that uses Lynx elements must import LYNX_ELEMENTS from @blotch/angular-lynx. Generated components already do.

Render a component

render() starts a standalone component the way your app starts, and returns queries bound to what it rendered:

src/app/greeting.test.ts
import { Component, signal } from '@angular/core';
import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
import { render } from '@blotch/angular-lynx-testing-library';

@Component({
  selector: 'app-greeting',
  imports: [LYNX_ELEMENTS],
  template: `<text>Hello {{ name() }}</text>`,
})
class Greeting {
  readonly name = signal('World');
}

it('renders a greeting', async () => {
  const { getByText } = await render(Greeting);
  expect(getByText('Hello World')).toBeTruthy();
});
  • render() is async. Await it. The first render has finished when it resolves.
  • Lynx elements are real elements in the test. A <view> renders as a <view> element and a <text> as a <text> element, so querySelector('text') works.
  • Change detection and the renderer are set up for you. You don't add provideZonelessChangeDetection() or provideRenderer().

Pass providers to swap in test values for your app's dependencies:

import { API_URL } from './api-url';

const { getByText } = await render(Greeting, {
  providers: [{ provide: API_URL, useValue: 'https://api.example.com' }],
});

Testing patterns uses this to fake native APIs.

Query what rendered

Use getBy* when the element must exist, and queryBy* when it may be absent:

src/app/product-list.test.ts
import { render, within } from '@blotch/angular-lynx-testing-library';
import { ProductList } from './product-list';

it('lists the products', async () => {
  const { container, getByText, queryByText } = await render(ProductList);

  expect(getByText('Gadget')).toBeTruthy();
  expect(queryByText('Sold out')).toBeNull();

  // Scope queries to one part of the tree with within():
  const secondRow = container.querySelectorAll('view')[1] as HTMLElement;
  expect(within(secondRow).getByText('Gadget')).toBeTruthy();
});

The library also exports screen, which runs queries against the whole document.

render() also returns:

  • container: the root <page> element that holds everything rendered.
  • componentRef: the component's ComponentRef, for reading its instance or its injector.
  • rerender(Component, options?): renders another component in its place.
  • asFragment(): a copy of the tree, for snapshot tests.
  • unmount(): destroys the app. The library also cleans up after every test by itself.

The API reference lists every option and type.

What jsdom tests can't cover

The tests run your app's main-thread copy in jsdom, not on a phone. Some things only exist on a device:

  • Native APIs. LynxNativeModule.call() returns undefined, and LynxTextMeasure.measure() throws. Provide fakes, as in Fake native APIs.
  • Data from the host app. No host app runs, so InitData and GlobalData hold nothing unless you provide them. See Fake host data.
  • Background-thread APIs. Only the copy of your app that draws the screen runs in a test. See Threads.
  • Main-thread scripts and gestures. jsdom has no native elements to move, so test dragging and gestures on a device.
  • Layout and native rendering. jsdom computes no layout, so sizes and positions are unknown, and layout events don't fire by themselves.

Check these by running the app in Lynx Explorer, or on a simulator with Run on iOS and Android.