mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
This commit replaces `fake_core` with the real `@angular/core` output. See previous commit for reasons. Overall, this commit: * Replaces references of `fake_core` * Fixes tests that were testing Angular compiler detection that _would_ already be flagged by type-checking of TS directly. We keep these tests for now, and add `@ts-ignore` to verify the Angular checks, in case type checking is disabled in user applications- but it's worth considering to remove these tests. Follow-up question/non-priority. * Adds `@ts-ignore` to the tests for `defer` 1P because the property is marked as `@internal` and now is (correctly) causing failures in the compiler test environment. * Fixes a couple of tests with typos, wrong properties etc that previously weren't detected! A good sign. PR Close #54650
1005 lines
33 KiB
TypeScript
1005 lines
33 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('ngtsc host directives compilation', () => {
|
|
let env!: NgtscTestEnvironment;
|
|
|
|
beforeEach(() => {
|
|
env = NgtscTestEnvironment.setup(testFiles);
|
|
env.tsconfig({strictTemplates: true});
|
|
});
|
|
|
|
function extractMessage(diag: ts.Diagnostic) {
|
|
return typeof diag.messageText === 'string' ? diag.messageText : diag.messageText.messageText;
|
|
}
|
|
|
|
it('should generate a basic hostDirectives definition', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true
|
|
})
|
|
export class DirectiveA {}
|
|
|
|
@Directive({
|
|
selector: '[dir-b]',
|
|
standalone: true
|
|
})
|
|
export class DirectiveB {}
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [DirectiveA, DirectiveB]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
const dtsContents = env.getContents('test.d.ts');
|
|
|
|
expect(jsContents).toContain('ɵɵdefineDirective({ type: DirectiveA');
|
|
expect(jsContents).toContain('ɵɵdefineDirective({ type: DirectiveB');
|
|
expect(jsContents)
|
|
.toContain('features: [i0.ɵɵHostDirectivesFeature([DirectiveA, DirectiveB])]');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵComponentDeclaration<MyComp, "my-comp", never, {}, {}, never, never, false, ' +
|
|
'[{ directive: typeof DirectiveA; inputs: {}; outputs: {}; }, ' +
|
|
'{ directive: typeof DirectiveB; inputs: {}; outputs: {}; }]>;');
|
|
});
|
|
|
|
it('should generate a hostDirectives definition that has inputs and outputs', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, Input, Output, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true
|
|
})
|
|
export class HostDir {
|
|
@Input() value: number;
|
|
@Input() color: string;
|
|
@Output() opened = new EventEmitter();
|
|
@Output() closed = new EventEmitter();
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [{
|
|
directive: HostDir,
|
|
inputs: ['value', 'color: colorAlias'],
|
|
outputs: ['opened', 'closed: closedAlias'],
|
|
}],
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
const dtsContents = env.getContents('test.d.ts');
|
|
|
|
expect(jsContents).toContain('ɵɵdefineDirective({ type: HostDir');
|
|
expect(jsContents)
|
|
.toContain(
|
|
'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' +
|
|
'inputs: ["value", "value", "color", "colorAlias"], ' +
|
|
'outputs: ["opened", "opened", "closed", "closedAlias"] }])]');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵComponentDeclaration<MyComp, "my-comp", never, {}, ' +
|
|
'{}, never, never, false, [{ directive: typeof HostDir; ' +
|
|
'inputs: { "value": "value"; "color": "colorAlias"; }; ' +
|
|
'outputs: { "opened": "opened"; "closed": "closedAlias"; }; }]>;');
|
|
});
|
|
|
|
it('should generate a hostDirectives definition that has aliased inputs and outputs', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, Input, Output, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true
|
|
})
|
|
export class HostDir {
|
|
@Input('valueAlias') value: number;
|
|
@Input('colorAlias') color: string;
|
|
@Output('openedAlias') opened = new EventEmitter();
|
|
@Output('closedAlias') closed = new EventEmitter();
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [{
|
|
directive: HostDir,
|
|
inputs: ['valueAlias', 'colorAlias: customColorAlias'],
|
|
outputs: ['openedAlias', 'closedAlias: customClosedAlias'],
|
|
}],
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
const dtsContents = env.getContents('test.d.ts');
|
|
|
|
expect(jsContents).toContain('ɵɵdefineDirective({ type: HostDir');
|
|
expect(jsContents)
|
|
.toContain(
|
|
'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' +
|
|
'inputs: ["valueAlias", "valueAlias", "colorAlias", "customColorAlias"], ' +
|
|
'outputs: ["openedAlias", "openedAlias", "closedAlias", "customClosedAlias"] }])]');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵComponentDeclaration<MyComp, "my-comp", never, {}, ' +
|
|
'{}, never, never, false, [{ directive: typeof HostDir; ' +
|
|
'inputs: { "valueAlias": "valueAlias"; "colorAlias": "customColorAlias"; }; ' +
|
|
'outputs: { "openedAlias": "openedAlias"; "closedAlias": "customClosedAlias"; }; }]>;');
|
|
});
|
|
|
|
it('should generate hostDirectives definitions for a chain of host directives', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component} from '@angular/core';
|
|
|
|
@Directive({standalone: true})
|
|
export class DirectiveA {
|
|
}
|
|
|
|
@Directive({
|
|
standalone: true,
|
|
hostDirectives: [DirectiveA],
|
|
})
|
|
export class DirectiveB {
|
|
}
|
|
|
|
@Directive({
|
|
standalone: true,
|
|
hostDirectives: [DirectiveB],
|
|
})
|
|
export class DirectiveC {
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [DirectiveC],
|
|
})
|
|
export class MyComp {
|
|
}
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
const dtsContents = env.getContents('test.d.ts');
|
|
|
|
expect(jsContents).toContain('ɵɵdefineDirective({ type: DirectiveA, standalone: true });');
|
|
expect(jsContents)
|
|
.toContain(
|
|
'ɵɵdefineDirective({ type: DirectiveB, standalone: true, ' +
|
|
'features: [i0.ɵɵHostDirectivesFeature([DirectiveA])] });');
|
|
expect(jsContents)
|
|
.toContain(
|
|
'ɵɵdefineDirective({ type: DirectiveC, standalone: true, ' +
|
|
'features: [i0.ɵɵHostDirectivesFeature([DirectiveB])] });');
|
|
expect(jsContents)
|
|
.toContain(
|
|
'ɵɵdefineComponent({ type: MyComp, selectors: [["my-comp"]],' +
|
|
' features: [i0.ɵɵHostDirectivesFeature([DirectiveC])]');
|
|
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵDirectiveDeclaration<DirectiveA, never, never, {}, ' +
|
|
'{}, never, never, true, never>;');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵDirectiveDeclaration<DirectiveB, never, never, {}, ' +
|
|
'{}, never, never, true, [{ directive: typeof DirectiveA; ' +
|
|
'inputs: {}; outputs: {}; }]>;');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵDirectiveDeclaration<DirectiveC, never, never, {}, ' +
|
|
'{}, never, never, true, [{ directive: typeof DirectiveB; ' +
|
|
'inputs: {}; outputs: {}; }]>;');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵComponentDeclaration<MyComp, "my-comp", never, {}, ' +
|
|
'{}, never, never, false, [{ directive: typeof DirectiveC; ' +
|
|
'inputs: {}; outputs: {}; }]>;');
|
|
});
|
|
|
|
it('should generate a hostDirectives definition with forward references', () => {
|
|
env.write('test.ts', `
|
|
import {Component, Directive, forwardRef, Input} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'my-component',
|
|
template: '',
|
|
hostDirectives: [forwardRef(() => DirectiveB)]
|
|
})
|
|
export class MyComponent {
|
|
}
|
|
|
|
@Directive({
|
|
standalone: true,
|
|
hostDirectives: [{directive: forwardRef(() => DirectiveA), inputs: ['value']}],
|
|
})
|
|
export class DirectiveB {
|
|
}
|
|
|
|
@Directive({standalone: true})
|
|
export class DirectiveA {
|
|
@Input() value: any;
|
|
}
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
const dtsContents = env.getContents('test.d.ts');
|
|
|
|
expect(jsContents)
|
|
.toContain(
|
|
'features: [i0.ɵɵHostDirectivesFeature(function () { return [DirectiveB]; })]');
|
|
expect(jsContents)
|
|
.toContain(
|
|
'features: [i0.ɵɵHostDirectivesFeature(function () { return [{ directive: DirectiveA, inputs: ["value", "value"] }]; })]');
|
|
expect(jsContents)
|
|
.toContain(
|
|
'ɵɵdefineDirective({ type: DirectiveA, ' +
|
|
'inputs: { value: "value" }, standalone: true });');
|
|
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵComponentDeclaration<MyComponent, "my-component", ' +
|
|
'never, {}, {}, never, never, false, [{ directive: typeof DirectiveB; ' +
|
|
'inputs: {}; outputs: {}; }]>;');
|
|
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵDirectiveDeclaration<DirectiveB, never, never, {}, ' +
|
|
'{}, never, never, true, [{ directive: typeof DirectiveA; ' +
|
|
'inputs: { "value": "value"; }; outputs: {}; }]>;');
|
|
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵDirectiveDeclaration<DirectiveA, never, never, ' +
|
|
'{ "value": { "alias": "value"; "required": false; }; }, {}, never, never, true, never>;');
|
|
});
|
|
|
|
it('should generate a definition if the host directives are imported from other files', () => {
|
|
env.write('dir-a.ts', `
|
|
import {Directive} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true
|
|
})
|
|
export class DirectiveA {}
|
|
`);
|
|
|
|
env.write('dir-b.ts', `
|
|
import {Directive, Input, Output, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-b]',
|
|
standalone: true
|
|
})
|
|
export class DirectiveB {
|
|
@Input() input: any;
|
|
@Output() output = new EventEmitter<any>();
|
|
}
|
|
`);
|
|
|
|
env.write('test.ts', `
|
|
import {Component, forwardRef} from '@angular/core';
|
|
import {DirectiveA} from './dir-a';
|
|
import {DirectiveB} from './dir-b';
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [
|
|
forwardRef(() => DirectiveA),
|
|
{
|
|
directive: forwardRef(() => DirectiveB),
|
|
inputs: ['input: inputAlias'],
|
|
outputs: ['output: outputAlias']
|
|
}
|
|
]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
const jsContents = env.getContents('test.js');
|
|
const dtsContents = env.getContents('test.d.ts');
|
|
|
|
expect(jsContents).toContain(`import { DirectiveA } from './dir-a'`);
|
|
expect(jsContents).toContain(`import { DirectiveB } from './dir-b'`);
|
|
expect(jsContents)
|
|
.toContain(
|
|
'features: [i0.ɵɵHostDirectivesFeature(function () { ' +
|
|
'return [i1.DirectiveA, { directive: i2.DirectiveB, inputs: ["input", "inputAlias"], ' +
|
|
'outputs: ["output", "outputAlias"] }]; })]');
|
|
|
|
expect(dtsContents).toContain('import * as i1 from "./dir-a";');
|
|
expect(dtsContents).toContain('import * as i2 from "./dir-b";');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵComponentDeclaration<MyComp, "my-comp", never, {}, ' +
|
|
'{}, never, never, false, [{ directive: typeof i1.DirectiveA; ' +
|
|
'inputs: {}; outputs: {}; }, { directive: typeof i2.DirectiveB; ' +
|
|
'inputs: { "input": "inputAlias"; }; outputs: { "output": "outputAlias"; }; }]>;');
|
|
});
|
|
|
|
it('should generate a hostDirectives definition referring to external directives', () => {
|
|
env.write('node_modules/external/index.d.ts', `
|
|
import {ɵɵDirectiveDeclaration} from '@angular/core';
|
|
|
|
export declare class ExternalDir {
|
|
static ɵdir: ɵɵDirectiveDeclaration<ExternalDir, '[test]', never,
|
|
{input: "input"}, {output: "output"}, never, never, true, never>;
|
|
}
|
|
`);
|
|
|
|
env.write('test.ts', `
|
|
import {Component, Directive, NgModule} from '@angular/core';
|
|
import {ExternalDir} from 'external';
|
|
|
|
@Component({
|
|
template: '',
|
|
hostDirectives: [{directive: ExternalDir, inputs: ['input: inputAlias'], outputs: ['output: outputAlias']}]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
env.driveMain();
|
|
const jsContents = env.getContents('test.js');
|
|
const dtsContents = env.getContents('test.d.ts');
|
|
|
|
expect(jsContents).toContain(`import * as i1 from "external";`);
|
|
expect(jsContents)
|
|
.toContain(
|
|
'features: [i0.ɵɵHostDirectivesFeature([{ directive: i1.ExternalDir, ' +
|
|
'inputs: ["input", "inputAlias"], outputs: ["output", "outputAlias"] }])]');
|
|
|
|
expect(dtsContents).toContain('import * as i1 from "external";');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵComponentDeclaration<MyComp, "ng-component", never, {}, ' +
|
|
'{}, never, never, false, [{ directive: typeof i1.ExternalDir; ' +
|
|
'inputs: { "input": "inputAlias"; }; outputs: { "output": "outputAlias"; }; }]>;');
|
|
});
|
|
|
|
it('should reference host directives by their external name', () => {
|
|
env.write('node_modules/external/index.d.ts', `
|
|
import {InternalDir} from './internal';
|
|
export {InternalDir as ExternalDir} from './internal';
|
|
`);
|
|
|
|
env.write('node_modules/external/internal.d.ts', `
|
|
export declare class InternalDir {
|
|
static ɵdir: ɵɵDirectiveDeclaration<ExternalDir, '[test]', never, never, {}, {}, never, true, never>;
|
|
}
|
|
`);
|
|
|
|
env.write('test.ts', `
|
|
import {Component} from '@angular/core';
|
|
import {ExternalDir} from 'external';
|
|
|
|
@Component({
|
|
template: '',
|
|
hostDirectives: [ExternalDir]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
env.driveMain();
|
|
const jsContents = env.getContents('test.js');
|
|
const dtsContents = env.getContents('test.d.ts');
|
|
|
|
expect(jsContents).toContain(`import * as i1 from "external";`);
|
|
expect(jsContents).toContain('features: [i0.ɵɵHostDirectivesFeature([i1.ExternalDir])]');
|
|
|
|
expect(dtsContents).toContain('import * as i1 from "external";');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'ɵɵComponentDeclaration<MyComp, "ng-component", never, {}, {}, ' +
|
|
'never, never, false, [{ directive: typeof i1.ExternalDir; inputs: {}; outputs: {}; }]>;');
|
|
});
|
|
|
|
it('should produce a template diagnostic if a required input from a host directive is missing',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, Input} from '@angular/core';
|
|
|
|
@Directive({standalone: true})
|
|
export class HostDir {
|
|
@Input({alias: 'inputAlias', required: true})
|
|
input: any;
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{directive: HostDir, inputs: ['inputAlias: customAlias']}],
|
|
standalone: true
|
|
})
|
|
export class Dir {}
|
|
|
|
@Component({
|
|
template: '<div dir></div>',
|
|
standalone: true,
|
|
imports: [Dir]
|
|
})
|
|
class App {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
|
|
expect(messages).toEqual(
|
|
[`Required input 'customAlias' from directive HostDir must be specified.`]);
|
|
});
|
|
|
|
it('should not produce a template diagnostic if a required input from a host directive is bound',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, Input} from '@angular/core';
|
|
|
|
@Directive({standalone: true})
|
|
export class HostDir {
|
|
@Input({alias: 'inputAlias', required: true})
|
|
input: any;
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{directive: HostDir, inputs: ['inputAlias: customAlias']}],
|
|
standalone: true
|
|
})
|
|
export class Dir {}
|
|
|
|
@Component({
|
|
template: '<div dir [customAlias]="value"></div>',
|
|
standalone: true,
|
|
imports: [Dir]
|
|
})
|
|
class App {
|
|
value = 123;
|
|
}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual([]);
|
|
});
|
|
|
|
describe('validations', () => {
|
|
it('should produce a diagnostic if a host directive is not standalone', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, NgModule} from '@angular/core';
|
|
|
|
@Directive()
|
|
export class HostDir {}
|
|
|
|
@Directive({
|
|
hostDirectives: [HostDir],
|
|
})
|
|
export class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(['Host directive HostDir must be standalone']);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive is not a directive', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Pipe, Component, NgModule} from '@angular/core';
|
|
|
|
@Pipe({name: 'hostDir'})
|
|
export class HostDir {}
|
|
|
|
@Directive({
|
|
hostDirectives: [HostDir],
|
|
})
|
|
export class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(
|
|
['HostDir must be a standalone directive to be used as a host directive']);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive is a component', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
template: '',
|
|
standalone: true,
|
|
})
|
|
export class HostComp {}
|
|
|
|
@Directive({
|
|
hostDirectives: [HostComp],
|
|
})
|
|
export class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(['Host directive HostComp cannot be a component']);
|
|
});
|
|
|
|
it('should produce a diagnostic if hostDirectives is not an array', () => {
|
|
env.write('test.ts', `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
template: '',
|
|
// @ts-ignore
|
|
hostDirectives: {}
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toContain('hostDirectives must be an array');
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive is not a reference', () => {
|
|
env.write('test.ts', `
|
|
import {Component} from '@angular/core';
|
|
|
|
const hostA = {} as any;
|
|
|
|
@Component({
|
|
template: '',
|
|
hostDirectives: [hostA]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(['Host directive must be a reference']);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive is not a reference to a class', () => {
|
|
env.write('test.ts', `
|
|
import {Component} from '@angular/core';
|
|
|
|
function hostA() {}
|
|
|
|
@Component({
|
|
template: '',
|
|
// @ts-ignore
|
|
hostDirectives: [hostA]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(['Host directive reference must be a class']);
|
|
});
|
|
|
|
it('should only produce a diagnostic once in a chain of directives', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, NgModule} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-b]',
|
|
})
|
|
export class HostDirB {}
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true,
|
|
hostDirectives: [HostDirB]
|
|
})
|
|
export class HostDirA {}
|
|
|
|
@Component({
|
|
selector: '[dir]',
|
|
template: '',
|
|
hostDirectives: [HostDirA],
|
|
})
|
|
export class Host {}
|
|
`);
|
|
|
|
// What we're checking here is that the diagnostics aren't produced recursively. If that
|
|
// were the case, the same diagnostic would show up more than once in the diagnostics since
|
|
// `HostDirB` is in the chain of both `Host` and `HostDirA`.
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(['Host directive HostDirB must be standalone']);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive output does not exist', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Output, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({standalone: true})
|
|
class HostDir {
|
|
@Output() foo = new EventEmitter();
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{
|
|
directive: HostDir,
|
|
outputs: ['doesNotExist'],
|
|
}]
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(
|
|
['Directive HostDir does not have an output with a public name of doesNotExist.']);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive output alias does not exist', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Output, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({standalone: true})
|
|
class HostDir {
|
|
@Output('alias') foo = new EventEmitter();
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{
|
|
directive: HostDir,
|
|
outputs: ['foo'],
|
|
}]
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(
|
|
['Directive HostDir does not have an output with a public name of foo.']);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive input does not exist', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
@Directive({standalone: true})
|
|
class HostDir {
|
|
@Input() foo: any;
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{
|
|
directive: HostDir,
|
|
inputs: ['doesNotExist'],
|
|
}]
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(
|
|
['Directive HostDir does not have an input with a public name of doesNotExist.']);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive input alias does not exist', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
@Directive({standalone: true})
|
|
class HostDir {
|
|
@Input('alias') foo: any;
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{directive: HostDir, inputs: ['foo']}],
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(
|
|
['Directive HostDir does not have an input with a public name of foo.']);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive tries to alias to an existing input',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
@Directive({selector: '[host-dir]', standalone: true})
|
|
class HostDir {
|
|
@Input('colorAlias') color?: string;
|
|
@Input() buttonColor?: string;
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{directive: HostDir, inputs: ['colorAlias: buttonColor']}]
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual([
|
|
'Cannot alias input colorAlias of host directive HostDir to buttonColor, because it ' +
|
|
'already has a different input with the same public name.'
|
|
]);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive tries to alias to an existing input alias',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
@Directive({selector: '[host-dir]', standalone: true})
|
|
class HostDir {
|
|
@Input('colorAlias') color?: string;
|
|
@Input('buttonColorAlias') buttonColor?: string;
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{directive: HostDir, inputs: ['colorAlias: buttonColorAlias']}]
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual(
|
|
['Cannot alias input colorAlias of host directive HostDir to buttonColorAlias, ' +
|
|
'because it already has a different input with the same public name.']);
|
|
});
|
|
|
|
it('should not produce a diagnostic if a host directive input aliases to the same name',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
@Directive({selector: '[host-dir]', standalone: true})
|
|
class HostDir {
|
|
@Input('color') color?: string;
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{directive: HostDir, inputs: ['color: buttonColor']}]
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual([]);
|
|
});
|
|
|
|
it('should produce a diagnostic if a host directive tries to alias to an existing output alias',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Output, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({selector: '[host-dir]', standalone: true})
|
|
class HostDir {
|
|
@Output('clickedAlias') clicked = new EventEmitter();
|
|
@Output('tappedAlias') tapped = new EventEmitter();
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{directive: HostDir, outputs: ['clickedAlias: tappedAlias']}]
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual([
|
|
'Cannot alias output clickedAlias of host directive HostDir ' +
|
|
'to tappedAlias, because it already has a different output with the same public name.'
|
|
]);
|
|
});
|
|
|
|
it('should not produce a diagnostic if a host directive output aliases to the same name',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Output, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({selector: '[host-dir]', standalone: true})
|
|
class HostDir {
|
|
@Output('clicked') clicked = new EventEmitter();
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[dir]',
|
|
hostDirectives: [{directive: HostDir, outputs: ['clicked: wasClicked']}]
|
|
})
|
|
class Dir {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
expect(messages).toEqual([]);
|
|
});
|
|
|
|
it('should produce a diagnostic if a required input is not exposed on the host', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, Input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true
|
|
})
|
|
export class HostDir {
|
|
@Input({required: true}) input: any;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [HostDir]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
|
|
expect(messages).toEqual(
|
|
[`Required input 'input' from host directive HostDir must be exposed.`]);
|
|
});
|
|
|
|
it('should use the public name when producing diagnostics about missing required inputs',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, Input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true
|
|
})
|
|
export class HostDir {
|
|
@Input({required: true, alias: 'inputAlias'}) input: any;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [HostDir]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
|
|
expect(messages).toEqual(
|
|
[`Required input 'inputAlias' from host directive HostDir must be exposed.`]);
|
|
});
|
|
|
|
it('should not produce required input diagnostic when exposed through alias', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, Input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true
|
|
})
|
|
export class HostDir {
|
|
@Input({required: true, alias: 'inputAlias'}) input: any;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [{directive: HostDir, inputs: ['inputAlias']}]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
|
|
expect(messages).toEqual([]);
|
|
});
|
|
|
|
it('should not produce required input diagnostic when exposed through alias to another alias',
|
|
() => {
|
|
env.write('test.ts', `
|
|
import {Directive, Component, Input} from '@angular/core';
|
|
|
|
@Directive({
|
|
selector: '[dir-a]',
|
|
standalone: true
|
|
})
|
|
export class HostDir {
|
|
@Input({required: true, alias: 'inputAlias'}) input: any;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-comp',
|
|
template: '',
|
|
hostDirectives: [{directive: HostDir, inputs: ['inputAlias: customAlias']}]
|
|
})
|
|
export class MyComp {}
|
|
`);
|
|
|
|
const messages = env.driveDiagnostics().map(extractMessage);
|
|
|
|
expect(messages).toEqual([]);
|
|
});
|
|
|
|
it('should not produce a diagnostic when exposing an aliased binding', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({
|
|
outputs: ['opened: triggerOpened'],
|
|
selector: '[trigger]',
|
|
standalone: true,
|
|
})
|
|
export class Trigger {
|
|
opened = new EventEmitter();
|
|
}
|
|
|
|
@Directive({
|
|
standalone: true,
|
|
selector: '[host]',
|
|
hostDirectives: [{directive: Trigger, outputs: ['triggerOpened']}]
|
|
})
|
|
export class Host {}
|
|
`);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(0);
|
|
});
|
|
|
|
it('should not produce a diagnostic when exposing an inherited aliased binding', () => {
|
|
env.write('test.ts', `
|
|
import {Directive, EventEmitter} from '@angular/core';
|
|
|
|
@Directive({standalone: true})
|
|
export abstract class Base {
|
|
opened = new EventEmitter();
|
|
}
|
|
|
|
@Directive({
|
|
outputs: ['opened: triggerOpened'],
|
|
selector: '[trigger]',
|
|
standalone: true,
|
|
})
|
|
export class Trigger extends Base {}
|
|
|
|
@Directive({
|
|
standalone: true,
|
|
selector: '[host]',
|
|
hostDirectives: [{directive: Trigger, outputs: ['triggerOpened: hostOpened']}]
|
|
})
|
|
export class Host {}
|
|
`);
|
|
|
|
const diags = env.driveDiagnostics();
|
|
expect(diags.length).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
});
|