Saltar al contenido principal
Neural UIv2.0.1Documentación
Ver v1 GitHub

Testing

Prueba tu aplicación Neural UI v2 con harnesses públicos de CDK, Vitest, TestBed, Playwright y helpers de hidratación.

Imports y entorno de pruebas

Un harness es un controlador para pruebas: ofrece operaciones como setValue o click sin que los tests de tu aplicación dependan del CSS interno. Neural UI 2.0.1 exporta ocho harnesses de Angular CDK desde @neural-ui/core/testing. Importa este entry point en archivos de pruebas, no en componentes de aplicación. No necesitas un paquete de testing separado de Neural.

Los ejemplos siguientes incluyen componentes host standalone completos y tests de Vitest, ejecutados con el builder de pruebas de Angular CLI. Utilizan @angular/cdk/testing/testbed, con las versiones de Angular/CDK de tu aplicación. Conserva la configuración normal de testing de Angular; estos ejemplos no necesitan una segunda llamada a TestBed.initTestEnvironment ni configurar Zone.js manualmente.

example.spec.ts
import { TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { NeuButtonHarness, detectStableChanges } from '@neural-ui/core/testing';

Prueba formularios y estado de aplicación

Comprueba el resultado que gobierna tu aplicación además del control renderizado. Este host enlaza los modelos value y checked, deriva disabled del consentimiento y guarda mediante neuClick. El test maneja Input, Checkbox, Switch y Button y comprueba la salida guardada. En Core 2.0.1, NeuInputHarness.setValue escribe el valor nativo sin disparar input. El ejemplo emite explícitamente ese evento en el input nativo y espera a Angular antes de comprobar el modelo.

Usa NeuButtonHarness.with({ text }) o un filtro selector de HarnessPredicate para distinguir varios controles. El harness estabiliza Angular durante sus interacciones. Tras cambiar directamente un signal del host, espera fixture.whenStable() o usa detectStableChanges(fixture); no añadas pausas arbitrarias para actualizar signals. Para Signal Forms, enlaza un formulario real en el host y verifica también validación, touched y disabled.

testing-controls.spec.ts
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 y Dialog: overlays y propuestas

Las opciones de Select pueden quedar fuera de la raíz del fixture. NeuSelectHarness busca sus opciones desde la raíz del documento internamente. Abre el control antes de leer getOptions; devuelve opciones renderizadas, no todos los elementos de una fuente lazy o virtual. Utiliza documentRootLoader cuando el propio host del harness quede fuera del fixture.

El input open de Dialog pertenece a tu aplicación. El botón de cierre emite closeRequested; llamar a harness.close() no impone otro estado al host. Este test rechaza una solicitud, acepta la siguiente y comprueba tanto el estado de aplicación como el panel renderizado. Mantén abierto solo el overlay que estés probando para evitar ambigüedad en consultas de opciones del documento.

testing-overlays.spec.ts
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 y DateInput

NeuTableHarness lee las filas y celdas renderizadas de tbody con índices desde cero. Las cabeceras pueden incluir columnas de interacción; las tablas virtuales o paginadas solo exponen las filas renderizadas en ese momento. Comprueba query y las propuestas de selección o edición en tu host cuando pruebes esas funciones.

NeuDateInputHarness.selectDay elige un número de día visible y habilitado; no elige mes ni año. Establece un CivilDate inicial determinista, locale y los límites antes de abrir el calendario, y comprueba el modelo de aplicación resultante en lugar de un texto dependiente del idioma.

testing-data.spec.ts
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);
  });
});

API pública de harnesses

Todos los métodos de interacción y lectura siguientes devuelven Promises; utiliza await. Los harnesses heredan las APIs de host y locators de Angular CDK. Solo NeuButtonHarness añade el predicado text y su fábrica with. Para otro harness, pasa new HarnessPredicate(NeuInputHarness, { selector: '...' }) a loader.getHarness; importa HarnessPredicate desde @angular/cdk/testing. No hay un harness público para cada componente de Neural. Para otros controles, utiliza roles y etiquetas accesibles o crea un harness CDK de tu aplicación.

Harness
Métodos públicos
Alcance y comportamiento
NeuButtonHarnesswith({ text?, selector?, ancestor? }); getText(); isDisabled(); isLoading(); click(); focus()button[neu-button]. text admite string o RegExp; loading lee aria-busy.
NeuInputHarnessgetValue(); setValue(value: string); getLabel(); getError(); isDisabled(); focus(); blur()neu-input. Etiqueta/error devuelven string | null. En 2.0.1 setValue no emite input: dispáralo explícitamente para actualizar el modelo del host.
NeuCheckboxHarnessisChecked(); isDisabled(); toggle(); getLabel()neu-checkbox. Maneja el checkbox nativo.
NeuSwitchHarnessisChecked(); isDisabled(); toggle(); getLabel()neu-switch. Lee checked y disabled del input nativo.
NeuSelectHarnessopen(); close(); isOpen(); getValueText(); getOptions(); selectOption(text: string | RegExp)neu-select. Lee opciones renderizadas del documento; lanza error si no encuentra la opción. close envía Escape.
NeuDateInputHarnessopen(); close(); isOpen(); getDisplayValue(); selectDay(day: number)neu-date-input. Selecciona un día visible habilitado; lanza error si no existe. No navega entre meses.
NeuTableHarnessgetRowCount(); getHeaderTexts(); getCellText(row: number, column: number)neu-table. Cuenta elementos tbody tr renderizados; las celdas empiezan en 0 y un índice inexistente lanza error.
NeuDialogHarnessisOpen(); getTitle(); getBodyText(); close()neu-dialog. Título/cuerpo pueden ser null. close pulsa el botón de cierre si existe; el host decide si acepta.

Helpers y diagnóstico de hidratación

Las utilidades de snapshots comparan la estructura descendiente y las relaciones id/for/ARIA. En una prueba SSR real, captura el DOM del servidor antes del bootstrap del cliente y el mismo subárbol tras hidratar. El pequeño ejemplo siguiente solo demuestra que el helper detecta cambios en las relaciones; no es una prueba de hidratación SSR. Que no haya diferencias no demuestra que funcionen los listeners, el foco, el texto, los estilos o la aplicación.

Los colectores CVA son utilidades de bajo nivel para objetos que implementan NeuCvaLike. Sustituyen el callback registrado: no los conectes a un formulario Angular activo ni presupongas que los controles Signal Forms de v2 implementan CVA. dispatchKeyboard crea un keydown sintético; usa un navegador real para recorrer con Tab, comprobar acciones nativas y probar interacción táctil.

Export
Firma / resultado
Comportamiento
detectStableChanges<T>(fixture: ComponentFixture<T>): Promise<void>Llama a detectChanges y espera whenStable.
dispatchKeyboard(target: EventTarget, key: string): KeyboardEventDispara un keydown con propagación y cancelable, y devuelve el evento.
collectCvaChanges<T>(component: NeuCvaLike<T>): T[]Registra un colector onChange; devuelve el array que recibirán futuras llamadas.
collectCvaTouches(component: NeuCvaLike<unknown>): boolean[]Registra un colector onTouched; añade true por cada llamada.
captureNeuHydrationSnapshot(root: ParentNode): NeuHydrationSnapshotCaptura descendientes, rutas estructurales, tags y atributos id/for/ARIA.
compareNeuHydrationSnapshots(server: NeuHydrationSnapshot, client: NeuHydrationSnapshot): string[]Devuelve diferencias estructurales y de relaciones; [] indica que coinciden las propiedades capturadas.
isNeuHydrationError(message: unknown): booleanReconoce NG05xx y mensajes de mismatch/error de hidratación. No instala un manejador de errores.
NeuCvaLike<T>writeValue(value: T | null | undefined); registerOnChange(fn); registerOnTouched(fn); setDisabledState(isDisabled: boolean)Contrato estructural para adaptadores CVA; no implica que todos los controles v2 implementen CVA.
NeuTextHarnessFiltersBaseHarnessFilters & { text?: string | RegExp }Filtro admitido por NeuButtonHarness.with.
NeuHydrationSnapshot / NeuHydrationElementSnapshotreadonly elements[] / path, tag, id, htmlFor, ariaControls, ariaDescribedBy, ariaLabelledBy, ariaActiveDescendantEstructuras de snapshot de solo lectura. Los valores de relaciones son string | null.
testing-utilities.spec.ts
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);
  });
});

Pruebas end-to-end con Playwright

Utiliza Playwright para tareas completas, foco de teclado, layouts adaptables e interacciones tras hidratar. Este ejemplo ejecutable usa la home real en español de este showcase: añade una tarea prioritaria y vacía la lista. Utiliza roles y nombres accesibles, sin selectores CSS internos de Neural ni pausas. Adapta la ruta, las etiquetas y el resultado esperado a tu aplicación.

Instala y configura @playwright/test con sus navegadores en tu proyecto E2E, guarda el archivo como showcase.spec.ts y ejecuta npx playwright test showcase.spec.ts. Por defecto utiliza el showcase público; define SHOWCASE_URL con el origen local o de preview para comprobar ese despliegue. Estas pruebas de navegador no utilizan TestbedHarnessEnvironment: ese entorno pertenece a los tests unitarios de Angular.

testing-browser.example.ts
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);
});

Ejecuta y amplía tus pruebas

Guarda los tests de Angular en archivos *.spec.ts y ejecútalos con el target de pruebas de Angular CLI de tu aplicación. Los ejemplos importan Vitest explícitamente. Respeta la configuración del runner de tu proyecto en lugar de mezclar su bootstrap con otro runner. Mantén los tests unitarios independientes de la web pública; el ejemplo separado de Playwright elige su propio origen.

Amplía cada tarea con entradas inválidas, estados disabled/readonly, propuestas rechazadas y comportamiento de teclado. Añade pruebas en navegador real de restauración de foco y layouts estrechos táctiles, además de HTML de servidor útil e interacción tras hidratar en aplicaciones SSR. La guía de verificación describe las comprobaciones de publicación; estos ejemplos muestran cómo empezar a escribir tus propios tests.

Terminal
npx ng test

Public Types

Consulta las definiciones de los tipos enlazados en esta página.