Skip to main content
Neural Nativev0.1.0-beta.1Documentation

Tools and public imports

These examples use NeuButton and NeuInput to test actions, unavailable states and text changes. Query the native controls by accessible role and name, and release each mounted application with cleanup().

component.test.ts
import { cleanup, fireEvent, render, screen, userEvent } from '@ng-native/testing';
import { afterEach, expect, test, vi } from 'vitest';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuInputComponent } from '@neural-ui/native/input';

afterEach(cleanup);

Configure the test environment

Install these development dependencies in your Angular Native 0.18.2 application. Keep testing aligned with the framework, and Vitest with its coverage package. No app.config.ts changes are needed.

terminal
npm install --save-dev @ng-native/testing@0.18.2 vitest@5 @vitest/coverage-v8@5

Create vitest.config.mts at the project root. The plugin compiles the imported Neural Native components; the animation alias uses the framework graph available in Node.

vitest.config.mts
import { ngNative } from '@ng-native/testing/vitest';
import { defineConfig } from 'vitest/config';
import { createRequire } from 'node:module';
import path from 'node:path';

const require = createRequire(import.meta.url);
const animations = path.join(
  path.dirname(require.resolve('@ng-native/components/package.json')),
  'dist/animations-web.js',
);

export default defineConfig({
  plugins: [ngNative({ inline: ['@neural-ui/native'] })],
  resolve: {
    alias: [{ find: /^@ng-native\/components\/animations$/, replacement: animations }],
  },
  test: {
    include: ['src/**/*.test.ts'],
    setupFiles: ['./test/setup-motion.mjs'],
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.test.ts', 'src/**/*.d.ts'],
      reporter: ['text', 'html', 'json-summary'],
      thresholds: { lines: 95, statements: 95, functions: 95, branches: 95 },
    },
  },
});

The default test environment exposes the Intl APIs Hermes provides. Collator, DateTimeFormat and NumberFormat remain available; APIs such as Segmenter and RelativeTimeFormat require an application polyfill. Load that polyfill in a Vitest setup file as well as in the application, so tests exercise the same support as the device.

Create test/setup-motion.mjs with this clock fallback. It keeps callbacks asynchronous and lets cleanup cancel them.

test/setup-motion.mjs
// Node has no display clock. Keep animations asynchronous and cancelable.
globalThis.requestAnimationFrame ??= callback =>
  setTimeout(() => callback(performance.now()), 16);
globalThis.cancelAnimationFrame ??= handle => clearTimeout(handle);

Test NeuButton

Save this as src/button.test.ts. Check that pressing emits the public output, while disabled and loading states prevent the action.

src/button.test.ts
import { cleanup, render, screen, userEvent } from '@ng-native/testing';
import { afterEach, expect, test, vi } from 'vitest';
import { NeuButtonComponent } from '@neural-ui/native/button';

afterEach(cleanup);

test('emits the save action once', async () => {
  const pressed = vi.fn();
  await render(NeuButtonComponent, {
    inputs: { label: 'Save changes' },
    on: { pressed },
  });

  await userEvent.setup().press(
    screen.getByRole('button', { name: 'Save changes' }),
  );

  expect(pressed).toHaveBeenCalledOnce();
});

test.each([{ disabled: true }, { loading: true }])(
  'does not submit while unavailable: %j',
  async state => {
    const pressed = vi.fn();
    await render(NeuButtonComponent, {
      inputs: { label: 'Save changes', ...state },
      on: { pressed },
    });

    await userEvent.setup().press(
      screen.getByRole('button', { name: 'Save changes' }),
    );

    expect(pressed).not.toHaveBeenCalled();
  },
);

Test NeuInput

Save this as src/input.test.ts. Editing must update the value and emit valueChange; readonly mode must prevent further edits.

src/input.test.ts
import { cleanup, fireEvent, render, screen } from '@ng-native/testing';
import { afterEach, expect, test, vi } from 'vitest';
import { NeuInputComponent } from '@neural-ui/native/input';

afterEach(cleanup);

test('updates the value model and emits its change', async () => {
  const valueChange = vi.fn();
  const result = await render(NeuInputComponent, {
    inputs: { label: 'Full name' },
    on: { valueChange },
  });

  await fireEvent.changeText(screen.getByLabelText('Full name'), 'Ada');

  expect(result.instance.value()).toBe('Ada');
  expect(valueChange).toHaveBeenCalledExactlyOnceWith('Ada');

  await result.rerender({ inputs: { readonly: true } });
  valueChange.mockClear();
  await fireEvent.changeText(screen.getByLabelText('Full name'), 'Grace');

  expect(result.instance.value()).toBe('Ada');
  expect(valueChange).not.toHaveBeenCalled();
});

Test form validation

This test uses Angular Signal Forms with a real NeuInput. It checks invalid values, touched state, the visible error and whether saving is enabled. Use the same configuration as above.

src/profile-form.test.ts
import { Component, computed, signal } from '@angular/core';
import { FormField, email, form, required } from '@angular/forms/signals';
import { NeuButtonComponent } from '@neural-ui/native/button';
import { NeuInputComponent } from '@neural-ui/native/input';
import { cleanup, fireEvent, render, screen, userEvent } from '@ng-native/testing';
import { afterEach, expect, test } from 'vitest';

@Component({
  selector: 'app-profile-form-test',
  imports: [FormField, NeuInputComponent, NeuButtonComponent],
  template: `
    <neu-input label="Email" [formField]="fields.email" [error]="error()" />
    <neu-button label="Save" [disabled]="fields().invalid()" (pressed)="saved.set(true)" />
  `,
})
class ProfileForm {
  readonly profile = signal({ email: '' });
  readonly fields = form(this.profile, path => {
    required(path.email, { message: 'Email is required.' });
    email(path.email, { message: 'Enter a valid email.' });
  });
  readonly saved = signal(false);
  readonly error = computed(() => {
    const field = this.fields.email();
    return field.touched() ? (field.errors()[0]?.message ?? '') : '';
  });
}

afterEach(cleanup);

test('validates the field before allowing a save', async () => {
  const result = await render(ProfileForm);
  const user = userEvent.setup();
  const save = screen.getByRole('button', { name: 'Save' });

  await user.press(save);
  expect(result.instance.saved()).toBe(false);

  await fireEvent.changeText(screen.getByLabelText('Email'), 'invalid');
  await fireEvent(screen.getByLabelText('Email'), 'blur');
  expect(result.instance.fields.email().touched()).toBe(true);
  expect(screen.getByRole('alert')).toBeTruthy();

  await fireEvent.changeText(screen.getByLabelText('Email'), 'ada@example.com');
  expect(result.instance.profile().email).toBe('ada@example.com');
  expect(result.instance.fields().valid()).toBe(true);
  expect(screen.queryByRole('alert')).toBeNull();

  await user.press(screen.getByRole('button', { name: 'Save' }));
  expect(result.instance.saved()).toBe(true);
});

Wait for an overlay state

Use findByRole for a control that appears after opening and waitFor for its disappearance. Assert the public model and output too. Avoid fixed sleeps; this checks the component state, while native animation appearance belongs to the device checks.

src/modal.test.ts
import { cleanup, render, screen, userEvent, waitFor } from '@ng-native/testing';
import { afterEach, expect, test, vi } from 'vitest';
import { NeuModalComponent } from '@neural-ui/native/modal';

afterEach(cleanup);

test('closes the modal through its public model and output', async () => {
  const closed = vi.fn();
  const result = await render(NeuModalComponent, {
    inputs: { title: 'Edit profile', open: false, closeLabel: 'Close profile' },
    on: { closed },
  });

  await result.rerender({ inputs: { open: true } });
  const close = await screen.findByRole('button', { name: 'Close profile' });
  await userEvent.setup().press(close);

  expect(result.instance.open()).toBe(false);
  expect(closed).toHaveBeenCalledExactlyOnceWith('action');
  await waitFor(() => {
    expect(screen.queryByRole('button', { name: 'Close profile' })).toBeNull();
  });
});

Run your tests

terminal
# Run component tests in your Angular Native application
npx vitest run

# Measure your application's source coverage
npx vitest run --coverage

# Open coverage/index.html to inspect uncovered paths

The coverage report measures your application source. The example configuration sets independent thresholds for lines, statements, functions and branches; adapt them to your project.

Complete the native checks

The simulated renderer checks state and events. Verify layout, animations, gestures, the keyboard and screen readers in the installed iOS and Android application.

Angular Native testing guide ↗