mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
39624c6b12
Previously the input flags were being generated as a reference to an enum member for better readability and under the assumption that minifiers would inline the values. That doesn't appear to be the case so these changes switch to using the literal values instead. PR Close #55215
376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
/**
|
|
* @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.io/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();
|
|
|
|
runInEachFileSystem(() => {
|
|
describe('initializer-based input() API', () => {
|
|
let env!: NgtscTestEnvironment;
|
|
|
|
beforeEach(() => {
|
|
env = NgtscTestEnvironment.setup(testFiles);
|
|
env.tsconfig({strictTemplates: true});
|
|
});
|
|
|
|
it('should handle a basic, primitive valued input', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class TestDir {
|
|
data = input('test');
|
|
}
|
|
`);
|
|
env.driveMain();
|
|
const js = env.getContents('test.js');
|
|
expect(js).toContain('inputs: { data: [1, "data"] }');
|
|
});
|
|
|
|
it('should fail if @Input is applied on signal input member', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Input, input} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class TestDir {
|
|
@Input() data = input('test');
|
|
}
|
|
`);
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics).toEqual([jasmine.objectContaining({
|
|
messageText: `Using @Input with a signal input is not allowed.`,
|
|
})]);
|
|
});
|
|
|
|
it('should fail if signal input is also declared in `inputs` decorator field.', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive({
|
|
inputs: ['data'],
|
|
})
|
|
export class TestDir {
|
|
data = input('test');
|
|
}
|
|
`);
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics).toEqual([
|
|
jasmine.objectContaining({
|
|
messageText: 'Input "data" is also declared as non-signal in @Directive.',
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('should fail if signal input declares a non-statically analyzable alias', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
const ALIAS = 'bla';
|
|
|
|
@Directive({
|
|
inputs: ['data'],
|
|
})
|
|
export class TestDir {
|
|
data = input('test', {alias: ALIAS});
|
|
}
|
|
`);
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics).toEqual([
|
|
jasmine.objectContaining({
|
|
messageText: 'Alias needs to be a string that is statically analyzable.',
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('should fail if signal input declares a non-statically analyzable options', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
const OPTIONS = {};
|
|
|
|
@Directive({
|
|
inputs: ['data'],
|
|
})
|
|
export class TestDir {
|
|
data = input('test', OPTIONS);
|
|
}
|
|
`);
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics).toEqual([
|
|
jasmine.objectContaining({
|
|
messageText: 'Argument needs to be an object literal that is statically analyzable.',
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('should fail if signal input is declared on static member', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class TestDir {
|
|
static data = input('test');
|
|
}
|
|
`);
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics).toEqual([
|
|
jasmine.objectContaining({
|
|
messageText: 'Input "data" is incorrectly declared as static member of "TestDir".',
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('should handle an alias configured, primitive valued input', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class TestDir {
|
|
data = input('test', {
|
|
alias: 'publicName',
|
|
});
|
|
}
|
|
`);
|
|
env.driveMain();
|
|
const js = env.getContents('test.js');
|
|
expect(js).toContain('inputs: { data: [1, "publicName", "data"] }');
|
|
});
|
|
|
|
it('should error if a required input declares an initial value', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class TestDir {
|
|
data = input.required({
|
|
initialValue: 'bla',
|
|
});
|
|
}
|
|
`);
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics[0].messageText).toEqual(jasmine.objectContaining({
|
|
messageText: 'No overload matches this call.',
|
|
}));
|
|
});
|
|
|
|
|
|
it('should handle a transform and required input', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class TestDir {
|
|
data = input.required({
|
|
transform: (v: string|number) => 'works',
|
|
});
|
|
}
|
|
`);
|
|
env.driveMain();
|
|
expect(env.getContents('test.js')).toContain(`inputs: { data: [1, "data"] }`);
|
|
expect(env.getContents('test.d.ts')).toContain('"required": true; "isSignal": true;');
|
|
expect(env.getContents('test.d.ts'))
|
|
.withContext(
|
|
'Expected no coercion member as input signal captures the write type of the transform')
|
|
.not.toContain('ngAcceptInputType');
|
|
});
|
|
|
|
|
|
it('should handle a non-primitive initial value', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class TestDir {
|
|
data = input(/default pattern/);
|
|
}
|
|
`);
|
|
env.driveMain();
|
|
expect(env.getContents('test.js')).toContain(`inputs: { data: [1, "data"] }`);
|
|
});
|
|
|
|
it('should report mixed two-way binding with a signal input', () => {
|
|
env.write('test.ts', `
|
|
import {Component, Directive, input, Output, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({standalone: true, selector: '[dir]'})
|
|
export class TestDir {
|
|
value = input('hello');
|
|
@Output() valueChange = new EventEmitter<string>();
|
|
}
|
|
|
|
@Component({
|
|
standalone: true,
|
|
template: \`<div dir [(value)]="value"></div>\`,
|
|
imports: [TestDir],
|
|
})
|
|
export class TestComp {
|
|
value = 1;
|
|
}
|
|
`);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(1);
|
|
expect(diags[0].messageText).toBe(`Type 'number' is not assignable to type 'string'.`);
|
|
});
|
|
|
|
describe('type checking', () => {
|
|
it('should work', () => {
|
|
env.write('test.ts', `
|
|
import {Component, Directive, input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[directiveName]',
|
|
standalone: true,
|
|
})
|
|
export class TestDir {
|
|
data = input(1);
|
|
}
|
|
|
|
@Component({
|
|
standalone: true,
|
|
template: \`<div directiveName [data]="false"></div>\`,
|
|
imports: [TestDir],
|
|
})
|
|
export class TestComp {
|
|
}
|
|
`);
|
|
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics.length).toBe(1);
|
|
expect(diagnostics[0].messageText)
|
|
.toBe(`Type 'boolean' is not assignable to type 'number'.`);
|
|
});
|
|
|
|
it('should work with transforms', () => {
|
|
env.write('test.ts', `
|
|
import {Component, Directive, input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[directiveName]',
|
|
standalone: true,
|
|
})
|
|
export class TestDir {
|
|
data = input.required({
|
|
transform: (v: string|number) => 'works',
|
|
});
|
|
}
|
|
|
|
@Component({
|
|
standalone: true,
|
|
template: \`<div directiveName [data]="false"></div>\`,
|
|
imports: [TestDir],
|
|
})
|
|
export class TestComp {
|
|
}
|
|
`);
|
|
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics.length).toBe(1);
|
|
expect(diagnostics[0].messageText)
|
|
.toBe(`Type 'boolean' is not assignable to type 'string | number'.`);
|
|
});
|
|
|
|
it('should report unset required inputs', () => {
|
|
env.write('test.ts', `
|
|
import {Component, Directive, input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[directiveName]',
|
|
standalone: true,
|
|
})
|
|
export class TestDir {
|
|
data = input.required<boolean>();
|
|
}
|
|
|
|
@Component({
|
|
standalone: true,
|
|
template: \`<div directiveName></div>\`,
|
|
imports: [TestDir],
|
|
})
|
|
export class TestComp {
|
|
}
|
|
`);
|
|
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics.length).toBe(1);
|
|
expect(diagnostics[0].messageText)
|
|
.toBe(`Required input 'data' from directive TestDir must be specified.`);
|
|
});
|
|
});
|
|
|
|
describe('diagnostics', () => {
|
|
it('should error when declared using an ES private field', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[directiveName]',
|
|
standalone: true,
|
|
})
|
|
export class TestDir {
|
|
#data = input.required<boolean>();
|
|
}
|
|
`);
|
|
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics.length).toBe(1);
|
|
expect(diagnostics).toEqual([jasmine.objectContaining<ts.Diagnostic>({
|
|
messageText: jasmine.objectContaining<ts.DiagnosticMessageChain>({
|
|
messageText: `Cannot use "input" on a class member that is declared as ES private.`,
|
|
}),
|
|
})]);
|
|
});
|
|
|
|
it('should error when declared using a `private` field', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[directiveName]',
|
|
standalone: true,
|
|
})
|
|
export class TestDir {
|
|
private data = input.required<boolean>();
|
|
}
|
|
`);
|
|
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics.length).toBe(1);
|
|
expect(diagnostics).toEqual([jasmine.objectContaining<ts.Diagnostic>({
|
|
messageText: jasmine.objectContaining<ts.DiagnosticMessageChain>({
|
|
messageText: `Cannot use "input" on a class member that is declared as private.`,
|
|
}),
|
|
})]);
|
|
});
|
|
|
|
it('should allow declaring using a `protected` field', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[directiveName]',
|
|
standalone: true,
|
|
})
|
|
export class TestDir {
|
|
protected data = input.required<boolean>();
|
|
}
|
|
`);
|
|
|
|
const diagnostics = env.driveDiagnostics();
|
|
expect(diagnostics.length).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
});
|