mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
0e081a6ef9
commit
bdecb0345a
3 changed files with 132 additions and 71 deletions
|
|
@ -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));');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
`
|
||||
|
|
|
|||
Loading…
Reference in a new issue