import { Injector, WritableSignal } from '@angular/core'; import { SchemaOrSchemaFn, FieldTree } from '@angular/forms/signals'; /** * `createForm()` / `toHaveFieldErrors` — Angular's signal forms in a spec, past the two things that * make the first one hard to write. * * **The injection context.** `form()` injects, so calling it in a `beforeEach` throws * `NG0203: The Injector token injection failed` — a message about `inject()` that never mentions * forms, and the repair (`{ injector: TestBed.inject(Injector) }`, or a * `TestBed.runInInjectionContext` around it) is the first thing every spec has to learn. Angular's * own testing guide calls the isolated schema test the default way to test a form, so this is the * step between a reader and the recommended pattern; `createForm()` is that step taken. * * **Reading the errors back.** `field().errors()` answers `RequiredValidationError` instances, not * `{ kind, message }` objects: they carry a `fieldTree` back-reference, so a `toEqual([{ kind: * 'required' }])` fails on a property nobody wrote, and every suite ends up with * `errors().some((error) => error.kind === 'required')` — which passes just as happily when the * field has three other errors nobody expected. `toHaveFieldErrors` compares the whole set by * `kind` (and `message` where the spec names one) and prints both sides when it does not match. * * The entry is narrow on purpose: this is the only file of the package that reaches * `@angular/forms`, which stays an optional peer paid for by the suites that import it. */ /** Where a form is built, when the `TestBed`'s own injector is not the one the spec wants. */ interface CreateFormOptions { /** * The injector `form()` runs in. Defaults to the `TestBed`'s — pass * `fixture.debugElement.injector` when a validator injects something a component provides. */ injector?: Injector; } /** One error as a spec names it: the kind alone, or the kind with the message it must carry. */ type FieldErrorMatch = string | { kind: string; message?: string; }; declare global { namespace Chai { interface Assertion { /** Compare the whole of a field's `errors()` with the kinds (and messages) the spec names. */ toHaveFieldErrors(expected: FieldErrorMatch | readonly FieldErrorMatch[]): void; } } } /** * Build a real signal form in the `TestBed`'s injection context. * * ```ts * const user = createForm({ email: '', name: '' }, (path) => { * required(path.email, { message: 'Email is required' }); * minLength(path.name, 2); * }); * * expect(user.email).toHaveFieldErrors([{ kind: 'required', message: 'Email is required' }]); * * user.email().value.set('ada@example.test'); * * expect(user.email).toHaveFieldErrors([]); * ``` * * Pass the model as a `signal()` when the spec asserts on it directly; pass the plain value and the * signal is made here, since `form().value()` reads it back either way. Everything else is * Angular's own: the tree, the states, the validators, the schema. */ declare function createForm(model: WritableSignal, schema?: SchemaOrSchemaFn, options?: CreateFormOptions): FieldTree; /** From the initial value, for the isolated schema test that never needs the model signal itself. */ declare function createForm(initialValue: TModel, schema?: SchemaOrSchemaFn, options?: CreateFormOptions): FieldTree; /** * Register {@link Chai.Assertion.toHaveFieldErrors} with the runner. Call once, from your setup file. * * @example * ```ts * registerFormMatchers(); // once, in the setup file * * expect(user.email).toHaveFieldErrors(['required']); * expect(user.email()).toHaveFieldErrors([]); // the state reads as well as the tree * ``` */ declare function registerFormMatchers(): void; export { type CreateFormOptions, type FieldErrorMatch, createForm, registerFormMatchers };