Quality
Testing
Test Neural Native components through their public API.
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().
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.
npm install --save-dev @ng-native/testing@0.18.2 vitest@5 @vitest/coverage-v8@5Create 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.
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.
// 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.
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.
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.
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.
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
# 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 pathsThe 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.