/
directive_spec.ts
163 lines (141 loc) Β· 6 KB
/
directive_spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* @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 {CssSelector, DirectiveMeta as T2DirectiveMeta, parseTemplate, R3TargetBinder, SelectorMatcher, TmplAstElement} from '@angular/compiler';
import * as ts from 'typescript';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports';
import {DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {getDeclaration, makeProgram} from '../../testing';
import {DirectiveDecoratorHandler} from '../src/directive';
runInEachFileSystem(() => {
let _: typeof absoluteFrom;
beforeEach(() => _ = absoluteFrom);
describe('DirectiveDecoratorHandler', () => {
it('should use the `ReflectionHost` to detect class inheritance', () => {
const {program} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Directive: any;',
},
{
name: _('/entry.ts'),
contents: `
import {Directive} from '@angular/core';
@Directive({selector: 'test-dir-1'})
export class TestDir1 {}
@Directive({selector: 'test-dir-2'})
export class TestDir2 {}
`,
},
]);
const analysis1 = analyzeDirective(program, 'TestDir1', /*hasBaseClass*/ false);
expect(analysis1.meta.usesInheritance).toBe(false);
const analysis2 = analyzeDirective(program, 'TestDir2', /*hasBaseClass*/ true);
expect(analysis2.meta.usesInheritance).toBe(true);
});
it('should record the source span of a Directive class type', () => {
const src = `
import {Directive} from '@angular/core';
@Directive({selector: 'test-dir'})
export class TestDir {}
`;
const {program} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Directive: any;',
},
{
name: _('/entry.ts'),
contents: src,
},
]);
const analysis = analyzeDirective(program, 'TestDir');
const span = analysis.meta.typeSourceSpan;
expect(span.toString()).toBe('TestDir');
expect(span.start.toString()).toContain('/entry.ts@5:22');
expect(span.end.toString()).toContain('/entry.ts@5:29');
});
it('should produce metadata compatible with template binding', () => {
const src = `
import {Directive, Input} from '@angular/core';
@Directive({selector: '[dir]'})
export class TestDir {
@Input('propName')
fieldName: string;
}
`;
const {program} = makeProgram([
{
name: _('/node_modules/@angular/core/index.d.ts'),
contents: 'export const Directive: any; export const Input: any;',
},
{
name: _('/entry.ts'),
contents: src,
},
]);
const analysis = analyzeDirective(program, 'TestDir');
const matcher = new SelectorMatcher<T2DirectiveMeta>();
const dirMeta: T2DirectiveMeta = {
exportAs: null,
inputs: analysis.inputs,
outputs: analysis.outputs,
isComponent: false,
name: 'Dir',
};
matcher.addSelectables(CssSelector.parse('[dir]'), dirMeta);
const {nodes} = parseTemplate('<div dir [propName]="expr"></div>', 'unimportant.html');
const binder = new R3TargetBinder(matcher).bind({template: nodes});
const propBinding = (nodes[0] as TmplAstElement).inputs[0];
const propBindingConsumer = binder.getConsumerOfBinding(propBinding);
// Assert that the consumer of the binding is the directive, which means that the metadata
// fed into the SelectorMatcher was compatible with the binder, and did not confuse property
// and field names.
expect(propBindingConsumer).toBe(dirMeta);
});
});
// Helpers
function analyzeDirective(program: ts.Program, dirName: string, hasBaseClass: boolean = false) {
class TestReflectionHost extends TypeScriptReflectionHost {
constructor(checker: ts.TypeChecker) {
super(checker);
}
hasBaseClass(_class: ClassDeclaration): boolean {
return hasBaseClass;
}
}
const checker = program.getTypeChecker();
const reflectionHost = new TestReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker, /*dependencyTracker*/ null);
const metaReader = new LocalMetadataRegistry();
const dtsReader = new DtsMetadataReader(checker, reflectionHost);
const scopeRegistry = new LocalModuleScopeRegistry(
metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]),
null);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader,
NOOP_DEFAULT_IMPORT_RECORDER, injectableRegistry, /*isCore*/ false,
/*annotateForClosureCompiler*/ false,
/*detectUndecoratedClassesWithAngularFeatures*/ false);
const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration);
const detected = handler.detect(DirNode, reflectionHost.getDecoratorsOfDeclaration(DirNode));
if (detected === undefined) {
throw new Error(`Failed to recognize @Directive (${dirName}).`);
}
const {analysis} = handler.analyze(DirNode, detected.metadata);
if (analysis === undefined) {
throw new Error(`Failed to analyze @Directive (${dirName}).`);
}
return analysis;
}
});