Testing
Test your Neural UI v2 application with public CDK harnesses, Vitest, TestBed, Playwright and hydration helpers.
Imports and test environment
import { TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { NeuButtonHarness, detectStableChanges } from '@neural-ui/core/testing';Test forms and application state
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { describe, expect, it } from 'vitest';
import { NeuButtonComponent } from '@neural-ui/core/button';
import { NeuInputComponent } from '@neural-ui/core/input';
import { NeuCheckboxComponent } from '@neural-ui/core/checkbox';
import { NeuSwitchComponent } from '@neural-ui/core/switch';
import {
detectStableChanges,
NeuButtonHarness,
NeuInputHarness,
NeuCheckboxHarness,
NeuSwitchHarness,
} from '@neural-ui/core/testing';
@Component({
imports: [NeuButtonComponent, NeuInputComponent, NeuCheckboxComponent, NeuSwitchComponent],
template: `
<neu-input label="Name" [(value)]="name" />
<neu-checkbox label="Accept terms" [(checked)]="accepted" />
<neu-switch label="Notifications" [(checked)]="notifications" />
<button neu-button [disabled]="!accepted()" (neuClick)="saved.set(name())">Save</button>
<output>{{ saved() }}</output>
`,
})
class ProfileHost {
readonly name = signal('');
readonly accepted = signal(false);
readonly notifications = signal(false);
readonly saved = signal('');
}
describe('Profile integration with Neural harnesses', () => {
it('updates application models and saves through neuClick', async () => {
await TestBed.configureTestingModule({ imports: [ProfileHost] }).compileComponents();
const fixture = TestBed.createComponent(ProfileHost);
await detectStableChanges(fixture);
const loader = TestbedHarnessEnvironment.loader(fixture);
const name = await loader.getHarness(NeuInputHarness);
const terms = await loader.getHarness(NeuCheckboxHarness);
const notifications = await loader.getHarness(NeuSwitchHarness);
const save = await loader.getHarness(NeuButtonHarness.with({ text: 'Save' }));
expect(await save.isDisabled()).toBe(true);
await name.setValue('Ada');
// In Core 2.0.1 setValue writes the native value without an input event.
const field = fixture.nativeElement.querySelector('neu-input input') as HTMLInputElement;
field.dispatchEvent(new Event('input', { bubbles: true }));
await fixture.whenStable();
await name.blur();
await terms.toggle();
await notifications.toggle();
expect(fixture.componentInstance.name()).toBe('Ada');
expect(await name.getValue()).toBe('Ada');
expect(await name.getLabel()).toBe('Name');
expect(await name.getError()).toBeNull();
expect(await terms.isChecked()).toBe(true);
expect(await notifications.isChecked()).toBe(true);
expect(await save.isDisabled()).toBe(false);
await save.click();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('output').textContent).toBe('Ada');
});
it('does not submit while the application disables the action', async () => {
await TestBed.configureTestingModule({ imports: [ProfileHost] }).compileComponents();
const fixture = TestBed.createComponent(ProfileHost);
await detectStableChanges(fixture);
const loader = TestbedHarnessEnvironment.loader(fixture);
await (await loader.getHarness(NeuButtonHarness)).click();
expect(fixture.componentInstance.saved()).toBe('');
});
});
Select and Dialog: overlays and proposals
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { describe, expect, it } from 'vitest';
import { NeuSelectComponent } from '@neural-ui/core/select';
import { NeuDialogComponent } from '@neural-ui/core/modal';
import { detectStableChanges, NeuSelectHarness, NeuDialogHarness } from '@neural-ui/core/testing';
@Component({
imports: [NeuSelectComponent, NeuDialogComponent],
template: `
<neu-select label="Destination" [options]="destinations" [(value)]="destination" />
<neu-dialog title="Review destination" [open]="open()" (closeRequested)="onClose()">
<p>Check your destination before continuing.</p>
</neu-dialog>
`,
})
class TravelHost {
readonly destinations = ['Madrid', 'Lisbon', 'Paris'];
readonly destination = signal<string | null>(null);
readonly open = signal(false);
readonly acceptClose = signal(false);
onClose(): void {
if (this.acceptClose()) this.open.set(false);
}
}
describe('Neural overlays and application-owned state', () => {
it('selects a real option from the document overlay', async () => {
await TestBed.configureTestingModule({ imports: [TravelHost] }).compileComponents();
const fixture = TestBed.createComponent(TravelHost);
await detectStableChanges(fixture);
const select = await TestbedHarnessEnvironment.loader(fixture).getHarness(NeuSelectHarness);
await select.open();
expect(await select.isOpen()).toBe(true);
expect(await select.getOptions()).toEqual(['Madrid', 'Lisbon', 'Paris']);
await select.selectOption('Lisbon');
expect(fixture.componentInstance.destination()).toBe('Lisbon');
expect(await select.getValueText()).toContain('Lisbon');
await select.close();
expect(await select.isOpen()).toBe(false);
});
it('closes Dialog only when the host accepts closeRequested', async () => {
await TestBed.configureTestingModule({ imports: [TravelHost] }).compileComponents();
const fixture = TestBed.createComponent(TravelHost);
fixture.componentInstance.open.set(true);
await detectStableChanges(fixture);
const root = TestbedHarnessEnvironment.documentRootLoader(fixture);
const dialog = await root.getHarness(NeuDialogHarness);
expect(await dialog.isOpen()).toBe(true);
expect(await dialog.getTitle()).toBe('Review destination');
expect(await dialog.getBodyText()).toContain('Check your destination');
await dialog.close();
expect(fixture.componentInstance.open()).toBe(true);
fixture.componentInstance.acceptClose.set(true);
await dialog.close();
await fixture.whenStable();
expect(fixture.componentInstance.open()).toBe(false);
expect(await dialog.isOpen()).toBe(false);
});
});
Table and DateInput
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { describe, expect, it } from 'vitest';
import { NeuTableComponent, type NeuTableColumn } from '@neural-ui/core/table';
import { NeuDateInputComponent, civilDate, type CivilDate } from '@neural-ui/core/date-input';
import { detectStableChanges, NeuTableHarness, NeuDateInputHarness } from '@neural-ui/core/testing';
interface Customer {
id: number;
name: string;
}
@Component({
imports: [NeuTableComponent, NeuDateInputComponent],
template: `
<neu-table [data]="customers" [columns]="columns" [rowKey]="rowKey" />
<neu-date-input label="Delivery" locale="en-US" [(value)]="delivery" />
`,
})
class DeliveryHost {
readonly customers: readonly Customer[] = [
{ id: 1, name: 'Ada' },
{ id: 2, name: 'Linus' },
];
readonly columns: readonly NeuTableColumn<Customer, 'name'>[] = [
{ id: 'name', field: 'name', header: 'Customer' },
];
readonly rowKey = (customer: Customer) => customer.id;
readonly delivery = signal<CivilDate | null>(civilDate('2026-10-15'));
}
describe('Neural data and date integration', () => {
it('reads rendered table rows and zero-based cells', async () => {
await TestBed.configureTestingModule({ imports: [DeliveryHost] }).compileComponents();
const fixture = TestBed.createComponent(DeliveryHost);
await detectStableChanges(fixture);
const table = await TestbedHarnessEnvironment.loader(fixture).getHarness(NeuTableHarness);
expect(await table.getRowCount()).toBe(2);
expect(await table.getHeaderTexts()).toEqual(['Customer']);
expect(await table.getCellText(0, 0)).toBe('Ada');
expect(await table.getCellText(1, 0)).toBe('Linus');
});
it('chooses a day in the month established by the initial value', async () => {
await TestBed.configureTestingModule({ imports: [DeliveryHost] }).compileComponents();
const fixture = TestBed.createComponent(DeliveryHost);
await detectStableChanges(fixture);
const date = await TestbedHarnessEnvironment.loader(fixture).getHarness(NeuDateInputHarness);
expect(await date.getDisplayValue()).not.toBe('');
await date.open();
expect(await date.isOpen()).toBe(true);
await date.selectDay(16);
expect(fixture.componentInstance.delivery()).toEqual(civilDate('2026-10-16'));
await date.close();
expect(await date.isOpen()).toBe(false);
});
});
Public harness API
Helpers and hydration diagnostics
import { describe, expect, it } from 'vitest';
import {
captureNeuHydrationSnapshot,
compareNeuHydrationSnapshots,
dispatchKeyboard,
isNeuHydrationError,
} from '@neural-ui/core/testing';
describe('Neural testing utilities', () => {
it('detects drift in accessibility relationships', () => {
const root = document.createElement('div');
root.innerHTML = '<label for="name">Name</label><input id="name">';
const before = captureNeuHydrationSnapshot(root);
expect(compareNeuHydrationSnapshots(before, captureNeuHydrationSnapshot(root))).toEqual([]);
root.querySelector('input')!.id = 'renamed';
expect(compareNeuHydrationSnapshots(before, captureNeuHydrationSnapshot(root))).toContain(
'input:1 id changed: name -> renamed',
);
expect(isNeuHydrationError('NG0500: Hydration Node Mismatch')).toBe(true);
expect(isNeuHydrationError('Network request failed')).toBe(false);
});
it('dispatches a cancellable, bubbling keydown event', () => {
const button = document.createElement('button');
button.addEventListener('keydown', (event) => {
if (event.key === 'Escape') event.preventDefault();
});
const event = dispatchKeyboard(button, 'Escape');
expect(event.defaultPrevented).toBe(true);
expect(event.bubbles).toBe(true);
});
});
End-to-end testing with Playwright
import { expect, test } from '@playwright/test';
// Override SHOWCASE_URL to run against a local or preview deployment.
test.use({ baseURL: process.env['SHOWCASE_URL'] ?? 'https://ui.neuralui.org' });
test('adds a priority task in the Spanish showcase', async ({ page }) => {
await page.goto('/v2/es');
const rejectCookies = page.getByRole('button', { name: 'Rechazar', exact: true });
if (await rejectCookies.isVisible()) await rejectCookies.click();
await page
.getByRole('textbox', { name: 'Nombre de la tarea', exact: true })
.fill('Preparar publicaciรณn');
const priority = page.getByRole('checkbox', { name: 'Marcar como prioritaria', exact: true });
await priority.press('Space');
await expect(priority).toBeChecked();
await page.getByRole('button', { name: 'Aรฑadir tarea', exact: true }).click();
const task = page.getByRole('listitem').filter({ hasText: 'Preparar publicaciรณn' });
await expect(task).toContainText('Prioritaria');
await page.getByRole('button', { name: 'Vaciar lista', exact: true }).click();
await expect(task).toHaveCount(0);
});
Run and extend your tests
npx ng testPublic Types
Inspect the definitions of types linked on this page.