/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/forms/signals/test/node/compat/compat.spec.ts
832 строки
23 KB
kirjs
refactor(forms): use when consistently for conditional rules and validators
07 май 2026, 00:10
07 май 2026, 00:10
df54e6a
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {ApplicationRef, Injector, signal} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {FormControl, FormGroup, Validators} from '@angular/forms'; import {compatForm, CompatValidationError} from '../../../compat/public_api'; import { disabled, email, FieldState, FieldTree, form, hidden, readonly, required, submit, TreeValidationResult, validate, validateTree, } from '../../../public_api'; function promiseWithResolvers<T>(): { promise: Promise<T>; resolve: (value: T | PromiseLike<T>) => void; reject: (reason?: any) => void; } { let resolve!: (value: T | PromiseLike<T>) => void; let reject!: (reason?: any) => void; const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return {promise, resolve, reject}; } describe('Forms compat', () => { it('should not error on a valid value', () => { const cat = signal({ name: 'pirojok-the-cat', age: new FormControl<number>(5, {nonNullable: true}), }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); const age = f.age(); expect(age.value()).toBe(5); expect(f.age().valid()).toBe(true); }); it('should handle and propagate errors', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f.age().value()).toBe(5); expect(f.age().valid()).toBe(true); expect(f().valid()).toBe(true); control.setValue(2); expect(f.age().value()).toBe(2); expect(f.age().valid()).toBe(false); expect(f().valid()).toBe(false); f.age().value.set(100); expect(f.age().value()).toBe(100); expect(f.age().valid()).toBe(true); expect(f().valid()).toBe(true); }); it('can be in root', () => { const catControl = new FormControl('meow', Validators.minLength(3)); const cat = signal(catControl); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f().value()).toBe('meow'); expect(f().valid()).toBe(true); }); it('picks up the value from a new form control', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f.age().value()).toBe(5); f().value.set({age: new FormControl(10), name: 'lol'}); expect(f.age().value()).toBe(10); const fc = new FormControl(25); cat.set({ name: 'meow-the-cat', age: fc, }); expect(f.age().value()).toBe(25); expect(f().value()).toEqual({ name: 'meow-the-cat', age: fc, }); }); it('handles multiple value and control changes', () => { const control = new FormControl(100, Validators.min(3)); const cat = signal(control); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f().value()).toBe(100); control.setValue(101); expect(f().value()).toBe(101); cat.set(new FormControl(6)); expect(f().value()).toBe(6); cat.set(new FormControl(7)); expect(f().value()).toBe(7); cat.set(new FormControl(8)); expect(f().value()).toBe(8); }); describe('validation', () => { it('picks up the validation from a new form control', () => { const control = new FormControl(5, Validators.min(10)); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f.age().valid()).toBeFalse(); expect(f.age().errors()).toEqual([ new CompatValidationError({control, kind: 'min', context: {min: 10, actual: 5}}), ]); f().value.set({age: new FormControl(4), name: 'lol'}); expect(f.age().valid()).toBeTrue(); }); it('allows to manually set errors', () => { const catControl = new FormControl('meow', Validators.minLength(3)); const cat = signal(catControl); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); catControl.setErrors({meow: true}); expect(f().errors()).toEqual([ new CompatValidationError({kind: 'meow', context: true, control: catControl}), ]); }); it('picks up the multiple errors from a new form control', () => { const control = new FormControl('pirojok-the-error', [ (c) => { return { [c.value]: {'meow': true}, 'error-1': 'error-1-content', 'error-2': 'error-2-content', }; }, ]); const cat = signal({ name: 'pirojok-the-cat', mistake: control, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f.mistake().valid()).toBeFalse(); expect(f.mistake().errors()).toEqual([ new CompatValidationError({kind: 'pirojok-the-error', context: {meow: true}, control}), new CompatValidationError({kind: 'error-1', context: 'error-1-content', control}), new CompatValidationError({kind: 'error-2', context: 'error-2-content', control}), ]); }); it('supports async validation', async () => { let resolve: Function = () => {}; const formControl = new FormControl<number>(5, { nonNullable: true, asyncValidators: () => { // can't use promiseWithResolver here, because this runs multiple times across tests. return new Promise<null>((r) => { resolve = r; }); }, }); const cat = signal({ name: 'pirojok-the-cat', age: formControl, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f.age().pending()).toBeTrue(); resolve(null); await TestBed.inject(ApplicationRef).whenStable(); expect(f.age().pending()).toBeFalse(); expect(f.age().valid()).toBeTrue(); f.age().control().setValue(18); expect(f.age().pending()).toBeTrue(); expect(f.age().valid()).toBeFalse(); resolve({'incorrect-cat': 123}); await TestBed.inject(ApplicationRef).whenStable(); expect(f.age().pending()).toBeFalse(); expect(f.age().errors()).toEqual([ new CompatValidationError({kind: 'incorrect-cat', context: 123, control: formControl}), ]); }); }); describe('state propagation', () => { it('propagates disabled state from parent', () => { const ageControl = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', age: ageControl, }); const f = compatForm( cat, (p) => { disabled(p, { when: ({value}) => { return value().name === 'disabled-cat'; }, }); }, { injector: TestBed.inject(Injector), }, ); expect(f.name().disabled()).withContext('name is initially enabled').toBeFalse(); expect(f.age().disabled()).withContext('age is initially enabled').toBeFalse(); f.name().value.set('disabled-cat'); expect(f.name().disabled()).withContext('name is disabled').toBeTrue(); expect(f.age().disabled()).withContext('age is disabled').toBeTrue(); f.name().value.set('enabled-cat'); expect(f.name().disabled()).withContext('name is enabled').toBeFalse(); expect(f.age().disabled()).withContext('age is enabled').toBeFalse(); ageControl.disable(); expect(f.name().disabled()).withContext('name is enabled').toBeFalse(); expect(f.age().disabled()).withContext('age is disabled').toBeTrue(); }); it('propagates hidden state from parent', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat1', age: control, }); const f = compatForm( cat, (p) => { hidden(p, { when: ({value}) => { return value().name === 'hidden-cat'; }, }); }, { injector: TestBed.inject(Injector), }, ); expect(f.name().hidden()).withContext('name is initially displayed').toBeFalse(); expect(f.age().hidden()).withContext('age is initially displayed').toBeFalse(); f.name().value.set('hidden-cat'); expect(f.name().hidden()).withContext('name is hidden').toBeTrue(); expect(f().hidden()).withContext('name is hidden').toBeTrue(); expect(f.age().hidden()).withContext('age is hidden').toBeTrue(); }); it('propagates touched state to parent', async () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f.name().touched()).withContext('name is initially not touched').toBeFalse(); expect(f().touched()).withContext('form is initially not touched').toBeFalse(); control.markAsTouched(); expect(f.age().touched()).withContext('age is touched, when control is touched').toBeTrue(); expect(f().touched()).withContext('form is touched when a child is touched').toBeTrue(); control.markAsUntouched(); expect(f.age().touched()).withContext('name is not touched when untouched').toBeFalse(); expect(f().touched()).withContext('name is not touched when child is untouched').toBeFalse(); f.age().markAsTouched(); expect(f.age().touched()).withContext('age is touched, when control is touched').toBeTrue(); expect(f().touched()).withContext('form is touched when a child is touched').toBeTrue(); }); it('picks up submittedState from parent', async () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = compatForm(cat, {injector: TestBed.inject(Injector)}); expect(f().submitting()).toBe(false); expect(f.age().submitting()).toBe(false); const {promise, resolve} = promiseWithResolvers<TreeValidationResult>(); const result = submit(f, { action: (field) => { expect(field.name().value()).toBe('pirojok-the-cat'); expect(field.age().control()).toBe(control); return promise; }, }); expect(f().submitting()).toBe(true); expect(f.age().submitting()).toBe(true); resolve([]); await result; expect(f().submitting()).toBe(false); expect(f.age().submitting()).toBe(false); }); it('propagates dirty state to parent', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f.age().dirty()).withContext('age is initially not dirty').toBeFalse(); expect(f().dirty()).withContext('form is initially not dirty').toBeFalse(); control.markAsDirty(); expect(f.age().dirty()).withContext('age is dirty, when control is dirty').toBeTrue(); expect(f().dirty()).withContext('form is dirty when the child is dirty').toBeTrue(); control.markAsPristine(); expect(f.age().dirty()).withContext('age is not dirty when marked as pristine').toBeFalse(); expect(f().dirty()) .withContext('age is not dirty when age is marked as pristine') .toBeFalse(); f.age().markAsDirty(); expect(f.age().dirty()).withContext('age is dirty, when control is dirty').toBeTrue(); expect(f().dirty()).withContext('form is dirty when the child is dirty').toBeTrue(); }); it('allows resetting state', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); control.markAsDirty(); control.markAsTouched(); expect(f.age().dirty()).toBeTrue(); expect(f.age().touched()).toBeTrue(); f.age().reset(); expect(f.age().dirty()).toBeFalse(); expect(f.age().touched()).toBeFalse(); expect(f().dirty()).toBeFalse(); expect(f().touched()).toBeFalse(); }); }); describe('exposing control', () => { it('works for control', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', address: { house: control, }, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f.address.house().control()).toBe(control); }); it('supports getting control from stateOf', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', address: { house: control, }, }); const f = compatForm( cat, (p) => { validate(p, ({stateOf}) => { return stateOf(p.address.house).control().value === 6 ? undefined : {kind: 'too small'}; }); }, { injector: TestBed.inject(Injector), }, ); expect(f().errors()).toEqual([{kind: 'too small', fieldTree: f}]); }); it('supports getting control from fieldTreeOf', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', address: { house: control, }, }); const f = compatForm( cat, (p) => { validate(p, ({fieldTreeOf}) => { return fieldTreeOf(p.address.house)().control().value === 6 ? undefined : {kind: 'too small'}; }); }, { injector: TestBed.inject(Injector), }, ); expect(f().errors()).toEqual([{kind: 'too small', fieldTree: f}]); }); it('fails for regular values', () => { const control = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', address: { house: control, }, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); // @ts-expect-error expect(() => f.name().control()).toThrowError(); }); }); it('disallows passing a path with a FormControl on the type level', () => { const control = new FormControl(5, {nonNullable: true, validators: [Validators.min(3)]}); const cat = signal({ name: 'pirojok-the-cat', age: control, }); compatForm( cat, (path) => { // @ts-expect-error required(path.age); // @ts-expect-error validate(path.age, () => { return undefined; }); }, { injector: TestBed.inject(Injector), }, ); }); it('allows to use form control values for validation', () => { const control = new FormControl(5, {nonNullable: true, validators: [Validators.min(3)]}); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = compatForm( cat, (path) => { required(path.name); validate(path.name, ({valueOf}) => { return valueOf(path.age) < 8 ? {kind: 'too small'} : undefined; }); }, { injector: TestBed.inject(Injector), }, ); expect(f.name().valid()).toBe(false); expect(f.name().errors()).toEqual([ { kind: 'too small', fieldTree: f.name, }, ]); control.setValue(10); expect(f.name().valid()).toBe(true); f.age().value.set(4); expect(f.name().valid()).toBe(false); f.age().value.update((value) => value + 6); expect(f.name().valid()).toBe(true); }); describe('arrays', () => { it('works with removing an element and bringing it back', async () => { const validCat = new FormControl('valid cat', {nonNullable: true}); const invalidCat = new FormControl('invalid cat', { nonNullable: true, validators: [Validators.maxLength(5)], }); const cats = signal({ cats: [validCat, invalidCat], }); const f = compatForm(cats, { injector: TestBed.inject(Injector), }); expect(f.cats[0]().value()).toBe('valid cat'); expect(f.cats[1]().value()).toBe('invalid cat'); expect(f.cats[0]().valid()).withContext('first cat is valid').toBe(true); expect(f.cats[1]().valid()).withContext('second cat is not valid').toBe(false); expect(f().valid()) .withContext('form is not valid because child validator fails') .toBe(false); cats.set({cats: []}); expect(f().valid()) .withContext('form is valid because invalid control has been removed') .toBe(true); cats.set({cats: [validCat]}); expect(f().valid()) .withContext('form is valid because only valid control was brought back') .toBe(true); expect(f.cats[0]().value()).toBe('valid cat'); expect(f.cats[1]).toBe(undefined); cats.set({cats: [invalidCat]}); expect(f().valid()) .withContext('form is invalid again, because invalid child is back') .toBe(false); expect(f.cats[0]().value()).toBe('invalid cat'); expect(f.cats[1]).toBe(undefined); }); }); describe('FormGroup', () => { it('is supported', () => { const cat = signal( new FormGroup({ name: new FormControl('pirojok-the-cat'), age: new FormControl(10), }), ); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f().value()).toEqual({ name: 'pirojok-the-cat', age: 10, }); expect(f().valid()).toEqual(true); }); it('uses the actual value, not rawValue', () => { const ageControl = new FormControl(10); const cat = signal( new FormGroup({ name: new FormControl('pirojok-the-cat'), age: ageControl, }), ); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); ageControl.disable(); expect(f().value()).toEqual({ name: 'pirojok-the-cat', age: 10, }); expect(f().valid()).toEqual(true); }); it('propagates validity and errors from child controls', () => { const ageControl = new FormControl(1, Validators.min(5)); const cat = signal( new FormGroup({ name: new FormControl('pirojok-the-cat'), age: ageControl, }), ); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); expect(f().valid()).toEqual(false); expect(f().errors()).toEqual([ new CompatValidationError({kind: 'min', control: ageControl, context: {min: 5, actual: 1}}), ]); }); }); it(`should not interpret 'FormControl' properties as subfields`, () => { const cat = signal(new FormControl('pirojok-the-cat')); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); // Does not pick up form control props as form children. // @ts-expect-error expect(f.value).toBe(undefined); // @ts-expect-error expect(f.setValue).toBe(undefined); // @ts-expect-error expect(f.child).toBe(undefined); }); describe('type test all rules', () => { it('compat form', () => { const control = new FormControl(5, {nonNullable: true, validators: [Validators.min(3)]}); const cat = signal({ name: 'pirojok-the-cat', age: control, }); compatForm( cat, (path) => { required(path.name); validate(path.name, ({valueOf}) => { return valueOf(path.age) < 8 ? {kind: 'too small'} : undefined; }); required(path.name, { when: ({valueOf}) => { return valueOf(path.age) < 8; }, }); validateTree(path.name, ({valueOf}) => { return valueOf(path.age) < 8 ? [] : []; }); readonly(path.name, { when: ({valueOf}) => { return valueOf(path.age) < 8; }, }); email(path.name, { error: ({valueOf}) => { return valueOf(path.age) < 8 ? [] : []; }, }); }, { injector: TestBed.inject(Injector), }, ); expect().nothing(); }); describe('regular forms', () => { it('throws when using valueOf on FormControl', () => { const control = new FormControl(5, {nonNullable: true, validators: [Validators.min(3)]}); const cat = signal({ name: 'pirojok-the-cat', age: control, }); const f = form( cat, (p) => { validate(p.name, ({valueOf}) => { valueOf(p.age); }); }, { injector: TestBed.inject(Injector), }, ); expect(() => f().valid()).toThrowError(/Tried to read an 'AbstractControl' value/); }); }); }); describe('type tests', () => { it('uses raw value', () => { const ageControl = new FormControl(10); const cat = signal( new FormGroup({ name: new FormControl('pirojok-the-cat', {nonNullable: true}), age: ageControl, }), ); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); const name: string = f().value().name; }); it('unwraps the value', () => { const ageControl = new FormControl(5, Validators.min(3)); const cat = signal({ name: 'pirojok-the-cat', age: ageControl, }); const f = compatForm(cat, { injector: TestBed.inject(Injector), }); const age: number | null = f.age().value(); // @ts-expect-error const notAge: string = f.age().value(); }); it('allows generic CompatValidationError', () => { const error = new CompatValidationError({ kind: 'min', context: {min: 3, max: 4}, control: new FormControl(), }); const min: number = error.context.min; // @ts-expect-error const notMin: string = error.context.min; }); it('Allows setting T value for non compat fields', () => { function setStateValue<T>(state: FieldState<T>, value: T) { state.value.set(value); } function setValueCompatState<T>(field: FieldState<T>, value: T) { field.value.set(field.value()); } function setValueRegularState<T>(field: FieldState<T>, value: T) { field.value.set(field.value()); } function setValue<T>(f: FieldTree<T>, value: T) { // @ts-expect-error f().value.set(value); } }); }); });