test(compiler-cli): set up TCB tests for signal forms

Adds TCB-specific tests for the generated code in signal forms since they tend to be a bit easier to read and follow.

(cherry picked from commit 748caf9a74)
This commit is contained in:
Kristiyan Kostadinov 2025-11-04 17:45:35 +01:00 committed by Andrew Scott
parent 0e081a6ef9
commit bdecb0345a
3 changed files with 132 additions and 71 deletions

View file

@ -2574,4 +2574,123 @@ describe('type check blocks', () => {
expect(block).toContain('_t1.input = (((this).value));');
});
});
describe('signal forms', () => {
let FieldMock: TestDirective;
beforeEach(() => {
setup([]);
FieldMock = {
type: 'directive',
name: 'Field',
selector: '[field]',
bestGuessOwningModule: {
specifier: '@angular/forms/signals',
resolutionContext: '',
},
inputs: {
field: 'field',
},
};
});
it('should generate a string field for an input without a type', () => {
const block = tcb('<input [field]="f"/>', [FieldMock]);
expect(block).toContain('var _t1 = null! as i0.Field<string>;');
expect(block).toContain('_t1.field = (((this).f));');
});
[
{inputType: 'text', expectedType: 'string'},
{inputType: 'radio', expectedType: 'string'},
{inputType: 'checkbox', expectedType: 'boolean'},
{inputType: 'number', expectedType: 'string | number'},
{inputType: 'range', expectedType: 'string | number'},
{inputType: 'datetime-local', expectedType: 'string | number'},
{inputType: 'date', expectedType: 'string | number | Date | null'},
{inputType: 'month', expectedType: 'string | number | Date | null'},
{inputType: 'time', expectedType: 'string | number | Date | null'},
{inputType: 'week', expectedType: 'string | number | Date | null'},
{inputType: 'unknown', expectedType: 'string'},
].forEach(({inputType, expectedType}) => {
it(`should generate a '${expectedType}' field for an input with a '${inputType}' type`, () => {
const block = tcb(`<input type="${inputType}" [field]="f"/>`, [FieldMock]);
expect(block).toContain(`var _t1 = null! as i0.Field<${expectedType}>;`);
expect(block).toContain('_t1.field = (((this).f));');
});
});
it('should generate a string field for a textarea', () => {
const block = tcb('<textarea [field]="f"></textarea>', [FieldMock]);
expect(block).toContain('var _t1 = null! as i0.Field<string>;');
expect(block).toContain('_t1.field = (((this).f));');
});
it('should generate a string field for a select', () => {
const block = tcb('<select [field]="f"></select>', [FieldMock]);
expect(block).toContain('var _t1 = null! as i0.Field<string>;');
expect(block).toContain('_t1.field = (((this).f));');
});
it('should generate a custom value control', () => {
const block = tcb('<custom-control [field]="f"/>', [
FieldMock,
{
type: 'directive',
name: 'CustomControl',
selector: 'custom-control',
inputs: {
value: {
classPropertyName: 'value',
bindingPropertyName: 'value',
required: false,
isSignal: true,
transform: null,
},
},
outputs: {
valueChange: 'valueChange',
},
},
]);
expect(block).toContain(
'var _t1 = null! as i0.Field<i1.ɵExtractFormControlValue<i0.CustomControl>>;',
);
expect(block).toContain('var _t2 = null! as i2.FormValueControl<unknown>;');
expect(block).toContain('if (_t2) _t2 = null! as i0.CustomControl;');
expect(block).toContain('_t1.field = (((this).f));');
});
it('should generate a custom checkbox control', () => {
const block = tcb('<custom-control [field]="f"/>', [
FieldMock,
{
type: 'directive',
name: 'CustomControl',
selector: 'custom-control',
inputs: {
checked: {
classPropertyName: 'checked',
bindingPropertyName: 'checked',
required: false,
isSignal: true,
transform: null,
},
},
outputs: {
checkedChange: 'checkedChange',
},
},
]);
expect(block).toContain(
'var _t1 = null! as i0.Field<i1.ɵExtractFormControlValue<i0.CustomControl>>;',
);
expect(block).toContain('var _t2 = null! as i2.FormCheckboxControl;');
expect(block).toContain('if (_t2) _t2 = null! as i0.CustomControl;');
expect(block).toContain('_t1.field = (((this).f));');
});
});
});

View file

@ -47,6 +47,7 @@ import {
LocalIdentifierStrategy,
LogicalProjectStrategy,
ModuleResolver,
OwningModule,
Reference,
ReferenceEmitter,
RelativePathStrategy,
@ -68,6 +69,7 @@ import {
import {NOOP_PERF_RECORDER} from '../../perf';
import {TsCreateProgramDriver} from '../../program_driver';
import {
AmbientImport,
ClassDeclaration,
isNamedClassDeclaration,
TypeScriptReflectionHost,
@ -341,6 +343,7 @@ export interface TestDirective
inputs?: string[];
outputs?: string[];
}[];
bestGuessOwningModule?: OwningModule | AmbientImport;
}
export interface TestPipe {
@ -350,6 +353,7 @@ export interface TestPipe {
pipeName: string;
type: 'pipe';
code?: string;
bestGuessOwningModule?: OwningModule | AmbientImport;
}
export type TestDeclaration = TestDirective | TestPipe;
@ -817,7 +821,7 @@ function prepareDeclarations(
} else if (decl.type === 'pipe') {
pipes.set(decl.pipeName, {
kind: MetaKind.Pipe,
ref: new Reference(resolveDeclaration(decl)),
ref: new Reference(resolveDeclaration(decl), decl.bestGuessOwningModule),
name: decl.pipeName,
nameExpr: null,
isStandalone: false,
@ -864,7 +868,7 @@ function getDirectiveMetaFromDeclaration(
) {
return {
name: decl.name,
ref: new Reference(resolveDeclaration(decl)),
ref: new Reference(resolveDeclaration(decl), decl.bestGuessOwningModule),
exportAs: decl.exportAs || null,
selector: decl.selector || null,
hasNgTemplateContextGuard: decl.hasNgTemplateContextGuard || false,

View file

@ -104,93 +104,31 @@ runInEachFileSystem(() => {
);
});
[
{inputType: 'text', expectedType: 'string'},
{inputType: 'radio', expectedType: 'string'},
{inputType: 'checkbox', expectedType: 'boolean'},
{inputType: 'number', expectedType: 'string | number'},
{inputType: 'range', expectedType: 'string | number'},
{inputType: 'datetime-local', expectedType: 'string | number'},
{inputType: 'date', expectedType: 'string | number | Date | null'},
{inputType: 'month', expectedType: 'string | number | Date | null'},
{inputType: 'time', expectedType: 'string | number | Date | null'},
{inputType: 'week', expectedType: 'string | number | Date | null'},
{inputType: 'unknown', expectedType: 'string'},
].forEach(({inputType, expectedType}) => {
it(`should infer an input with '${inputType}' type as a '${expectedType}' field`, () => {
env.write(
'test.ts',
`
it('should infer the type of the field from the input `type`', () => {
env.write(
'test.ts',
`
import {Component, signal} from '@angular/core';
import {Field, form} from '@angular/forms/signals';
@Component({
template: '<input type="${inputType}" [field]="f"/>',
template: '<input type="date" [field]="f"/>',
imports: [Field]
})
export class Comp {
f = form(signal(null as unknown));
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(extractMessage(diags[0])).toBe(
`Type '() => FieldState<unknown, string | number>' is not assignable to type '() => FieldState<${expectedType}, string | number>'.`,
);
});
});
it('should infer a `textarea` as a string field', () => {
env.write(
'test.ts',
`
import {Component, signal} from '@angular/core';
import {Field, form} from '@angular/forms/signals';
@Component({
template: '<textarea [field]="f"></textarea>',
imports: [Field]
})
export class Comp {
f = form(signal(0));
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(extractMessage(diags[0])).toBe(
`Type '() => FieldState<number, string | number>' is not assignable to type '() => FieldState<string, string | number>'.`,
`Type '() => FieldState<unknown, string | number>' is not assignable to type '() => FieldState<string | number | Date | null, string | number>'.`,
);
});
it('should infer a `select` as a string field', () => {
env.write(
'test.ts',
`
import {Component, signal} from '@angular/core';
import {Field, form} from '@angular/forms/signals';
@Component({
template: '<select [field]="f"></select>',
imports: [Field]
})
export class Comp {
f = form(signal(0));
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(extractMessage(diags[0])).toBe(
`Type '() => FieldState<number, string | number>' is not assignable to type '() => FieldState<string, string | number>'.`,
);
});
it('should infer the type of a custom form field control', () => {
it('should infer the type of a custom value control', () => {
env.write(
'test.ts',
`