/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/compiler-cli/test/ngtsc/signal_forms_spec.ts
788 строк
24 KB
Matthieu Riegler
refactor(compiler): remove explicit `strict: true`
12 авг 2026, 21:03
12 авг 2026, 21:03
f1c0c40
Код
Авторство
О чём код?
/*! * @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 ts from 'typescript'; import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '../../src/ngtsc/testing'; import {NgtscTestEnvironment} from './env'; const testFiles = loadStandardTestFiles({forms: true}); runInEachFileSystem(() => { describe('signal forms', () => { let env!: NgtscTestEnvironment; beforeEach(() => { env = NgtscTestEnvironment.setup(testFiles); env.tsconfig(); }); function extractMessage(diag: ts.Diagnostic) { return typeof diag.messageText === 'string' ? diag.messageText : diag.messageText.messageText; } it('should check that the field is present', () => { env.write( 'test.ts', ` import {Component} from '@angular/core'; import {FormField} from '@angular/forms/signals'; @Component({ template: '<input [formField]="null"/>', imports: [FormField] }) export class Comp {} `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Type 'null' is not assignable to type 'Field<any, string | number>'.`, ); }); it('should check that the field type is correct', () => { env.write( 'test.ts', ` import {Component} from '@angular/core'; import {FormField} from '@angular/forms/signals'; @Component({ template: '<input formField="staticString"/>', imports: [FormField] }) export class Comp {} `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Type 'string' is not assignable to type 'Field<any, string | number>'.`, ); }); it('should treat FormField directives not coming from the forms module as regular directives', () => { env.write( 'field.ts', ` import {Directive, input} from '@angular/core'; @Directive({selector: '[formField]'}) export class FormField { readonly formField = input.required<string>(); } `, ); env.write( 'test.ts', ` import {Component} from '@angular/core'; import {FormField} from './field'; @Component({ template: '<input [formField]="null"/>', imports: [FormField] }) export class Comp {} `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe(`Type 'null' is not assignable to type 'string'.`); }); it('should reject a numeric field without null on a bare input', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input [formField]="f"/>', imports: [FormField] }) export class Comp { f = form(signal(0)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toContain( `is not assignable to type '{ (): string; set: (v: string) => void; } | { (): number | null; set: (v: number | null) => void; }'`, ); }); it('should allow a string field on a bare input', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input [formField]="f"/>', imports: [FormField] }) export class Comp { f = form(signal('')); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should allow a number|null field on a text input', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input type="text" [formField]="f"/>', imports: [FormField] }) export class Comp { f = form(signal<number | null>(42)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should reject a string|null field on a text input', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input type="text" [formField]="f"/>', imports: [FormField] }) export class Comp { f = form(signal<string | null>(null)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toContain( `is not assignable to type '{ (): string; set: (v: string) => void; } | { (): number | null; set: (v: number | null) => void; }'`, ); }); it('should reject a boolean field on a text input', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input type="text" [formField]="f"/>', imports: [FormField] }) export class Comp { f = form(signal(true)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toContain( `is not assignable to type '{ (): string; set: (v: string) => void; } | { (): number | null; set: (v: number | null) => void; }'`, ); }); it('should reject a number field on a textarea', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<textarea [formField]="f"></textarea>', imports: [FormField] }) export class Comp { f = form(signal(0)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe(`Type 'number' is not assignable to type 'string'.`); }); it('should infer the type of the field from the input `type`', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input type="date" [formField]="f"/>', imports: [FormField] }) export class Comp { f = form(signal({})); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Type '{}' is not assignable to type 'string | number | Date | null'.`, ); }); it('should allow min/max bindings on date inputs', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input type="date" [formField]="f" min="2026-01-01" max="2026-12-31"/>', imports: [FormField] }) export class Comp { f = form(signal(new Date('2026-01-15'))); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should prohibit min/max bindings on non-date inputs', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input type="number" [formField]="f" min="1" max="10"/>', imports: [FormField] }) export class Comp { f = form(signal(5)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(2); expect(extractMessage(diags[0])).toBe( `Setting the 'min' attribute is not allowed on nodes using the '[formField]' directive`, ); expect(extractMessage(diags[1])).toBe( `Setting the 'max' attribute is not allowed on nodes using the '[formField]' directive`, ); }); it('should infer the type of a custom value control', () => { env.write( 'test.ts', ` import {Component, signal, model} from '@angular/core'; import {FormField, form, FormValueControl} from '@angular/forms/signals'; interface User { firstName: string; lastName: string; } @Component({selector: 'user-control', template: ''}) export class UserControl implements FormValueControl<User> { readonly value = model<User>({firstName: 'Frodo', lastName: 'Baggins'}); } @Component({ template: '<user-control [formField]="f"/>', imports: [FormField, UserControl] }) export class Comp { f = form(signal({name: 'Bilbo'})); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Type '{ name: string; }' is missing the following properties from type 'User': firstName, lastName`, ); }); it('should infer the type of a custom checkbox control', () => { env.write( 'test.ts', ` import {Component, signal, model} from '@angular/core'; import {FormField, form, FormCheckboxControl} from '@angular/forms/signals'; @Component({selector: 'my-checkbox', template: ''}) export class MyCheckbox implements FormCheckboxControl { readonly checked = model(false); } @Component({ template: '<my-checkbox [formField]="f"/>', imports: [FormField, MyCheckbox] }) export class Comp { f = form(signal('')); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe(`Type 'string' is not assignable to type 'boolean'.`); }); it('should infer the type of a generic custom control', () => { env.write( 'test.ts', ` import {Component, signal, model} from '@angular/core'; import {FormField, form, FormValueControl} from '@angular/forms/signals'; @Component({selector: 'custom-control', template: ''}) export class CustomControl<T> implements FormValueControl<T> { readonly value = model.required<T>(); } @Component({ template: \` <custom-control [formField]="f" #comp/> {{expectsString(comp.value())}} \`, imports: [FormField, CustomControl] }) export class Comp { f = form(signal(0)); expectsString(value: string) {} } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Argument of type 'number' is not assignable to parameter of type 'string'.`, ); }); it('should not report a custom control that conforms to `FormValueControl`', () => { env.write( 'test.ts', ` import {Component, signal, model} from '@angular/core'; import {FormField, form, FormValueControl} from '@angular/forms/signals'; @Component({ selector: 'string-control', template: '' }) class StringControl implements FormValueControl<string> { value = model(''); } @Component({ selector: 'app-root', imports: [FormField, StringControl], template: '<string-control [formField]="field" />', }) class App { field = form(signal('')); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should report unsupported property bindings on a field', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input type="number" [formField]="f" [max]="10"/>', imports: [FormField] }) export class Comp { f = form(signal(0)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Binding to '[max]' is not allowed on nodes using the '[formField]' directive`, ); }); it('should report unsupported attribute bindings on a field', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input [formField]="f" [attr.maxlength]="maxLength"/>', imports: [FormField] }) export class Comp { f = form(signal('')); maxLength = 10; } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Binding to '[attr.maxlength]' is not allowed on nodes using the '[formField]' directive`, ); }); it('should report unsupported property bindings on a field with a custom control', () => { env.write( 'test.ts', ` import {Component, signal, model, input} from '@angular/core'; import {FormField, form, FormValueControl} from '@angular/forms/signals'; @Component({selector: 'custom-control', template: ''}) export class CustomControl implements FormValueControl<number> { readonly value = model<number>(0); readonly max = input<number | undefined>(1); } @Component({ template: '<custom-control [formField]="f" [max]="2"/>', imports: [FormField, CustomControl] }) export class Comp { f = form(signal(0)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Binding to '[max]' is not allowed on nodes using the '[formField]' directive`, ); }); it('should allow binding to `value` on radio controls', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: \` <form> <input type="radio" value="a" [formField]="f"> <input type="radio" value="b" [formField]="f"> <input type="radio" value="c" [formField]="f"> </form> \`, imports: [FormField] }) export class Comp { f = form(signal('a'), {name: 'test'}); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should check that the radio button value is a string', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: \` <form> <input type="radio" [value]="num" [formField]="f"> </form> \`, imports: [FormField] }) export class Comp { f = form(signal('a'), {name: 'test'}); num = 1; } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(diags[0].messageText).toBe(`Type 'number' is not assignable to type 'string'.`); }); it('should report unsupported static attributes of a field', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input value="Hello" [formField]="f"/>', imports: [FormField] }) export class Comp { f = form(signal('')); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Setting the 'value' attribute is not allowed on nodes using the '[formField]' directive`, ); }); it('should check that a custom value control conforms to FormValueControl', () => { env.write( 'test.ts', ` import {Component, signal, model, input} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({selector: 'user-control', template: ''}) export class UserControl { readonly value = model<number>(0); required = input<number>(0); } @Component({ template: '<user-control [formField]="f"/>', imports: [FormField, UserControl] }) export class Comp { f = form(signal(1)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe(`Type 'boolean' is not assignable to type 'number'.`); }); it('should check that a custom checkbox control conforms to FormCheckboxControl', () => { env.write( 'test.ts', ` import {Component, signal, model, input} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({selector: 'user-control', template: ''}) export class UserControl { readonly checked = model<boolean>(false); required = input<number>(0); } @Component({ template: '<user-control [formField]="f"/>', imports: [FormField, UserControl] }) export class Comp { f = form(signal(true)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe(`Type 'boolean' is not assignable to type 'number'.`); }); it('should not report `value` as a missing required input when the `FormField` directive is present', () => { env.write( 'test.ts', ` import {Component, signal, model} from '@angular/core'; import {FormField, form, FormValueControl} from '@angular/forms/signals'; @Component({selector: 'custom-control', template: ''}) export class CustomControl implements FormValueControl<string> { readonly value = model.required<string>(); } @Component({ template: '<custom-control [formField]="f"/>', imports: [FormField, CustomControl] }) export class Comp { f = form(signal('')); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should not report `checked` as a missing required input when the `FormField` directive is present', () => { env.write( 'test.ts', ` import {Component, signal, model} from '@angular/core'; import {FormField, form, FormCheckboxControl} from '@angular/forms/signals'; @Component({selector: 'custom-control', template: ''}) export class CustomControl implements FormCheckboxControl { readonly checked = model.required<boolean>(); } @Component({ template: '<custom-control [formField]="f"/>', imports: [FormField, CustomControl] }) export class Comp { f = form(signal(false)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should not check field on native control that has a ControlValueAccessor directive', () => { env.write( 'test.ts', ` import {Component, Directive, signal} from '@angular/core'; import {ControlValueAccessor} from '@angular/forms'; import {FormField, form} from '@angular/forms/signals'; @Directive({selector: '[customCva]'}) export class CustomCva implements ControlValueAccessor { writeValue() {} registerOnChange() {} registerOnTouched() {} } @Component({ template: '<input customCva [formField]="f"/>', imports: [FormField, CustomCva] }) export class Comp { f = form(signal(0)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should not check field on native control that has a directive inheriting from a ControlValueAccessor', () => { env.write( 'test.ts', ` import {Component, Directive, signal} from '@angular/core'; import {ControlValueAccessor} from '@angular/forms'; import {FormField, form} from '@angular/forms/signals'; @Directive() export class Grandparent implements ControlValueAccessor { writeValue() {} registerOnChange() {} registerOnTouched() {} } @Directive() export class Parent extends Grandparent {} @Directive({selector: '[customCva]'}) export class CustomCva extends Parent {} @Component({ template: '<input customCva [formField]="f"/>', imports: [FormField, CustomCva] }) export class Comp { f = form(signal(0)); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should infer an input with a dynamic `type` as being any of the other types', () => { env.write( 'test.ts', ` import {Component, signal} from '@angular/core'; import {FormField, form} from '@angular/forms/signals'; @Component({ template: '<input [type]="type" [formField]="f"/>', imports: [FormField] }) export class Comp { type = ''; f = form(signal({test: true})); } `, ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(extractMessage(diags[0])).toBe( `Type '{ test: boolean; }' is not assignable to type 'string | number | boolean | Date | null'.`, ); }); }); });